stream()
Descrição
Um stream é uma estrutura de dados reativa, similar às células em aplicações de planilha.
Por exemplo, em uma planilha, se A1 = B1 + C1
, então alterar o valor de B1
ou C1
automaticamente altera o valor de A1
.
De forma similar, você pode fazer um stream depender de outros streams, de modo que alterar o valor de um atualize automaticamente o outro. Isso é útil quando você tem cálculos complexos e deseja executá-los apenas quando necessário, em vez de, por exemplo, a cada redesenho.
Streams NÃO são incluídos na distribuição principal do Mithril.js. Para incluir o módulo Streams, use:
var Stream = require('mithril/stream');
Você também pode baixar o módulo diretamente se o seu ambiente não suportar uma toolchain (cadeia de ferramentas) de bundling:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Quando carregado diretamente com uma tag <script>
(em vez de ser importado), a biblioteca de stream será exposta como window.m.stream
. Se window.m
já estiver definido (por exemplo, porque você também usa o script principal do Mithril.js), ele se anexará ao objeto existente. Caso contrário, ele cria um novo window.m
. Se você quiser usar streams em conjunto com o Mithril.js como tags de script brutas, você deve incluir o Mithril.js em sua página antes de mithril/stream
, porque mithril
sobrescreverá o objeto window.m
definido por mithril/stream
. Isso não é uma preocupação quando as bibliotecas são consumidas como módulos CommonJS (usando require(...)
).
Assinatura
Cria um stream.
stream = Stream(value)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
value | any | Não | Se este argumento estiver presente, o valor do stream é definido para ele. |
retorna | Stream | Retorna um stream. |
Membros estáticos
Stream.combine
Cria um stream computado que é atualizado reativamente se algum de seus upstreams (streams de origem) for atualizado. Veja combinando streams.
stream = Stream.combine(combiner, streams)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
combiner | (Stream..., Array) -> any | Sim | Veja o argumento combiner. |
streams | Array<Stream> | Sim | Uma lista de streams a serem combinados. |
retorna | Stream | Retorna um stream. |
combiner
Especifica como o valor de um stream computado é gerado. Veja combinando streams.
any = combiner(streams..., changed)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
streams... | lista variável de Streams | Não | Lista variável de zero ou mais streams que correspondem aos streams passados como o segundo argumento para stream.combine . |
changed | Array<Stream> | Sim | Lista de streams que foram afetados por uma atualização. |
retorna | any | Retorna um valor computado. |
Stream.merge
Cria um stream cujo valor é o array de valores de um array de streams.
stream = Stream.merge(streams)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
streams | Array<Stream> | Sim | Uma lista de streams. |
retorna | Stream | Retorna um stream cujo valor é um array de valores de stream de entrada. |
Stream.scan
Cria um novo stream com os resultados de chamar a função em cada valor no stream com um acumulador e o valor de entrada.
Note que você pode impedir que streams dependentes sejam atualizados retornando o valor especial stream.SKIP
dentro da função acumuladora.
stream = Stream.scan(fn, accumulator, stream)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Sim | Uma função que recebe um acumulador e um parâmetro de valor e retorna um novo valor de acumulador do mesmo tipo. |
accumulator | any | Sim | O valor inicial para o acumulador. |
stream | Stream | Sim | Stream contendo os valores. |
retorna | Stream | Retorna um novo stream contendo o resultado. |
Stream.scanMerge
Recebe um array de pares de streams e funções de scan e mescla todos esses streams usando as funções fornecidas em um único stream.
stream = Stream.scanMerge(pairs, accumulator)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Sim | Um array de tuplas de stream e funções de scan. |
accumulator | any | Sim | O valor inicial para o acumulador. |
retorna | Stream | Retorna um novo stream contendo o resultado. |
Stream.lift
Cria um stream computado que é atualizado reativamente se algum de seus upstreams (streams de origem) for atualizado. Veja combinando streams. Diferente de combine
, os streams de entrada são um número variável de argumentos (em vez de um array) e o callback recebe os valores do stream em vez de streams. Não há parâmetro changed
. Esta é geralmente uma função mais amigável para aplicações do que combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
lifter | (any...) -> any | Sim | Veja o argumento lifter. |
streams... | lista de Streams | Sim | Streams a serem lifted (elevados). |
retorna | Stream | Retorna um stream. |
lifter
Especifica como o valor de um stream computado é gerado. Veja combinando streams.
any = lifter(streams...)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
streams... | lista variável de Streams | Não | Lista variável de zero ou mais valores que correspondem aos valores dos streams passados para stream.lift . |
retorna | any | Retorna um valor computado. |
Stream.SKIP
Um valor especial que pode ser retornado para callbacks de stream para pular a execução de streams dependentes.
Stream["fantasy-land/of"]
Este método é funcionalmente idêntico a stream
. Ele existe para estar em conformidade com a especificação Applicative de Fantasy Land. Para mais informações, veja a seção O que é Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
value | any | Não | Se este argumento estiver presente, o valor do stream é definido para ele. |
retorna | Stream | Retorna um stream. |
Membros de instância
stream.map
Cria um stream dependente cujo valor é definido para o resultado da função de callback. Este método é um alias de stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
callback | any -> any | Sim | Um callback cujo valor de retorno se torna o valor do stream. |
retorna | Stream | Retorna um stream. |
stream.end
Um stream co-dependente que cancela o registro de streams dependentes quando definido como true
. Veja estado finalizado.
endStream = stream().end
stream["fantasy-land/of"]
Este método é funcionalmente idêntico a stream
. Ele existe para estar em conformidade com a especificação Applicative de Fantasy Land. Para mais informações, veja a seção O que é Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
value | any | Não | Se este argumento estiver presente, o valor do stream é definido para ele. |
retorna | Stream | Retorna um stream. |
stream["fantasy-land/map"]
Cria um stream dependente cujo valor é definido para o resultado da função de callback. Veja encadeando streams.
Este método existe para estar em conformidade com a especificação Applicative de Fantasy Land. Para mais informações, veja a seção O que é Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
callback | any -> any | Sim | Um callback cujo valor de retorno se torna o valor do stream. |
retorna | Stream | Retorna um stream. |
stream["fantasy-land/ap"]
O nome deste método significa apply
(aplicar). Se um stream a
tem uma função como seu valor, outro stream b
pode usá-lo como o argumento para b.ap(a)
. Chamar ap
chamará a função com o valor do stream b
como seu argumento, e retornará outro stream cujo valor é o resultado da chamada da função. Este método existe para estar em conformidade com a especificação Applicative de Fantasy Land. Para mais informações, veja a seção O que é Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argumento | Tipo | Obrigatório | Descrição |
---|---|---|---|
apply | Stream | Sim | Um stream cujo valor é uma função. |
retorna | Stream | Retorna um stream. |
Uso básico
Streams não fazem parte da distribuição principal do Mithril.js. Para incluí-los em um projeto, importe seu módulo:
var stream = require('mithril/stream');
Streams como variáveis
stream()
retorna um stream. Em seu nível mais básico, um stream funciona de forma semelhante a uma variável ou uma propriedade com métodos getter e setter: ele pode manter o estado, que pode ser modificado.
var username = stream('John');
console.log(username()); // registra "John"
username('John Doe');
console.log(username()); // registra "John Doe"
A principal diferença é que um stream é uma função e, portanto, pode ser composto em funções de ordem superior.
var users = stream();
// Solicita usuários de um servidor usando a API fetch
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
No exemplo acima, o stream users
é preenchido com os dados da resposta quando a solicitação é resolvida.
Vinculações bidirecionais
Streams também podem ser preenchidos a partir de callbacks de eventos e similares.
// Um stream
var user = stream('');
// Uma vinculação bidirecional para o stream
m('input', {
oninput: function (e) {
user(e.target.value); // Atualiza o stream `user` com o valor do input
},
value: user(), // Define o valor do input com o valor do stream `user`
});
No exemplo acima, quando o usuário digita no input, o stream user
é atualizado para o valor do campo de input.
Propriedades computadas
Streams são úteis para implementar propriedades computadas que são atualizadas automaticamente:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // registra "hello-world"
No exemplo acima, o valor de slug
é computado quando title
é atualizado, não quando slug
é lido.
É claro que também é possível computar propriedades com base em múltiplos streams:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // registra "John Doe"
firstName('Mary');
console.log(fullName()); // registra "Mary Doe"
Propriedades computadas no Mithril.js são atualizadas atomicamente: streams que dependem de múltiplos streams nunca serão chamados mais de uma vez por atualização de valor, não importa quão complexo seja o grafo de dependência da propriedade computada.
Encadeando streams
Streams podem ser encadeados usando o método map
. Um stream encadeado também é conhecido como um stream dependente.
// Stream pai
var value = stream(1);
// Stream dependente
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // registra 2
Streams dependentes são reativos: seus valores são atualizados sempre que o valor de seu stream pai é atualizado. Isso acontece independentemente de o stream dependente ter sido criado antes ou depois que o valor do stream pai foi definido.
Você pode impedir que streams dependentes sejam atualizados retornando o valor especial stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// Nunca executa
});
Combinando streams
Streams podem depender de mais de um stream pai. Esses tipos de streams podem ser criados via stream.merge()
.
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // registra "hello world"
Ou você pode usar a função auxiliar stream.lift()
.
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // registra "hello world"
Há também um método de nível inferior chamado stream.combine()
que expõe os próprios streams nas computações reativas para casos de uso mais avançados.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // registra 12
Um stream pode depender de qualquer número de streams e é garantido que ele seja atualizado atomicamente. Por exemplo, se um stream A tem dois streams dependentes B e C, e um quarto stream D é dependente de ambos B e C, o stream D só será atualizado uma vez se o valor de A mudar. Isso garante que o callback para o stream D nunca seja chamado com valores instáveis, como quando B tem um novo valor, mas C tem o valor antigo. A atomicidade também traz os benefícios de desempenho de não recomputar streams dependentes desnecessariamente.
Você pode impedir que streams dependentes sejam atualizados retornando o valor especial stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// Nunca executa
});
Estados do Stream
A qualquer momento, um stream pode estar em um de três estados: pendente (pending), ativo (active) e finalizado (ended).
Estado pendente
Streams pendentes podem ser criados chamando stream()
sem parâmetros.
var pending = stream();
Se um stream depende de mais de um stream e qualquer um de seus streams pais está em um estado pendente, o stream dependente também está em um estado pendente e não atualiza seu valor.
var a = stream(5);
var b = stream(); // Stream pendente
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // registra undefined
No exemplo acima, added
é um stream pendente, porque seu pai b
também está pendente.
Isso também se aplica a streams dependentes criados via stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // registra undefined porque `doubled` está pendente
Estado ativo
Quando um stream recebe um valor, ele se torna ativo (a menos que o stream seja finalizado).
var stream1 = stream('hello'); // stream1 está ativo
var stream2 = stream(); // stream2 começa pendente
stream2('world'); // então se torna ativo
Um stream dependente com múltiplos pais se torna ativo se todos os seus pais estiverem ativos.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
No exemplo acima, o stream a
está ativo, mas b
está pendente. Definir b("world")
faria com que b
se tornasse ativo e, portanto, greeting
também se tornaria ativo e seria atualizado para ter o valor "hello world"
.
Estado finalizado
Um stream pode parar de afetar seus streams dependentes chamando stream.end(true)
. Isso efetivamente remove a conexão entre um stream e seus streams dependentes.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // Define para o estado finalizado
value(5);
console.log(doubled());
// registra undefined porque `doubled` não depende mais de `value`
Streams finalizados ainda têm semântica de contêiner de estado, ou seja, você ainda pode usá-los como getters e setters, mesmo depois de finalizados.
var value = stream(1);
value.end(true); // Define para o estado finalizado
console.log(value(1)); // registra 1
value(2);
console.log(value()); // registra 2
Finalizar um stream pode ser útil nos casos em que um stream tem um tempo de vida limitado (por exemplo, reagir a eventos mousemove
apenas enquanto um elemento DOM está sendo arrastado, mas não depois que ele é solto).
Serializando streams
Streams implementam um método .toJSON()
. Quando um stream é passado como o argumento para JSON.stringify()
, o valor do stream é serializado.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // registra 123
Streams não acionam o redesenho
Ao contrário de bibliotecas como Knockout, os streams do Mithril.js não acionam o redesenho de templates. O redesenho acontece em resposta a manipuladores de eventos definidos nas views de componentes do Mithril.js, mudanças de rota ou após a resolução de chamadas m.request
.
Se o redesenho for desejado em resposta a outros eventos assíncronos (por exemplo, setTimeout
/setInterval
, assinatura de websocket, manipulador de eventos de biblioteca de terceiros, etc.), você deve chamar manualmente m.redraw()
.
O que é Fantasy Land
Fantasy Land especifica a interoperabilidade de estruturas algébricas comuns. Em termos mais simples, isso significa que bibliotecas que estão em conformidade com as especificações de Fantasy Land podem ser usadas para escrever código genérico de estilo funcional que funciona independentemente de como essas bibliotecas implementam os constructs.
Por exemplo, digamos que queremos criar uma função genérica chamada plusOne
. A implementação ingênua seria assim:
function plusOne(a) {
return a + 1;
}
O problema com esta implementação é que ela só pode ser usada com um número. No entanto, é possível que qualquer lógica que produza um valor para a
também possa produzir um estado de erro (envolvido em um maybe ou um either de uma biblioteca como Sanctuary ou Ramda-Fantasy), ou poderia ser um stream do Mithril.js, um stream do Flyd, etc. Idealmente, não gostaríamos de escrever uma função semelhante para cada tipo possível que a
poderia ter e não gostaríamos de escrever código de encapsulamento/desencapsulamento/tratamento de erros repetidamente.
É aqui que Fantasy Land pode ajudar. Vamos reescrever essa função em termos de uma álgebra de Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Agora este método funciona com qualquer functor compatível com Fantasy Land, como R.Maybe
, S.Either
, stream
, etc.
Este exemplo pode parecer convoluted, mas é um trade-off em complexidade: a implementação ingênua plusOne
faz sentido se você tem um sistema simples e só incrementa números, mas a implementação de Fantasy Land se torna mais poderosa se você tem um sistema grande com muitas abstrações e algoritmos reutilizados.
Ao decidir se você deve adotar Fantasy Land, você deve considerar a familiaridade da sua equipe com programação funcional e ser realista em relação ao nível de disciplina que sua equipe pode se comprometer a manter a qualidade do código (versus a pressão de escrever novos recursos e cumprir prazos). A programação de estilo funcional depende fortemente da compilação, curadoria e domínio de um grande conjunto de funções pequenas e precisamente definidas e, portanto, não é adequada para equipes que não têm práticas de documentação sólidas e/ou falta de experiência em linguagens orientadas a funcional.