stream()
Descripción
Un Stream es una estructura de datos reactiva, similar a las celdas en las aplicaciones de hojas de cálculo.
Por ejemplo, en una hoja de cálculo, si A1 = B1 + C1
, entonces cambiar el valor de B1
o C1
actualiza automáticamente el valor de A1
.
De manera similar, puedes hacer que un flujo dependa de otros flujos, de modo que cambiar el valor de uno actualice automáticamente el otro. Esto es útil cuando tienes cálculos costosos y quieres ejecutarlos solo cuando sea necesario, en lugar de hacerlo, por ejemplo, en cada redibujo.
Los streams no se incluyen con la distribución principal de Mithril.js. Para incluir el módulo Streams, usa:
var Stream = require('mithril/stream');
También puedes descargar el módulo directamente si tu entorno no admite una cadena de herramientas de empaquetado:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Cuando se carga directamente con la etiqueta <script>
(en lugar de requerirlo), la biblioteca de streams estará disponible como window.m.stream
. Si window.m
ya está definido (por ejemplo, porque también usas el script principal de Mithril.js), se adjuntará al objeto existente. De lo contrario, crea un nuevo window.m
. Si quieres usar streams en conjunto con Mithril.js como etiquetas de script sin procesar, debes incluir Mithril.js en tu página antes de mithril/stream
, porque de lo contrario mithril
sobrescribirá el objeto window.m
definido por mithril/stream
. Esto no es un problema cuando las bibliotecas se consumen como módulos CommonJS (usando require(...)
).
Firma
Crea un stream
stream = Stream(value)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
value | any | No | Si este argumento está presente, el valor del stream se establece en él. |
devuelve | Stream | Devuelve un stream. |
Miembros estáticos
Stream.combine
Crea un stream calculado que se actualiza reactivamente si alguno de sus streams ascendentes se actualiza. Ver la sección Combinación de streams.
stream = Stream.combine(combiner, streams)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
combiner | (Stream..., Array) -> any | Sí | Ver argumento combiner. |
streams | Array<Stream> | Sí | Una lista de streams para combinar. |
devuelve | Stream | Devuelve un stream. |
combiner
Especifica cómo se genera el valor de un stream calculado. Ver la sección Combinación de streams.
any = combiner(streams..., changed)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
streams... | splat de Streams | No | Splat de cero o más streams que corresponden a los streams pasados como el segundo argumento a stream.combine . |
changed | Array<Stream> | Sí | Lista de streams que se vieron afectados por una actualización. |
devuelve | any | Devuelve un valor calculado. |
Stream.merge
Crea un stream cuyo valor es el array de valores de un array de streams.
stream = Stream.merge(streams)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
streams | Array<Stream> | Sí | Una lista de streams. |
devuelve | Stream | Devuelve un stream cuyo valor es un array con los valores de los streams de entrada. |
Stream.scan
Crea un nuevo stream con los resultados de llamar a la función en cada valor del stream con un acumulador y el valor entrante.
Ten en cuenta que se puede evitar que los streams dependientes se actualicen devolviendo el valor especial stream.SKIP
dentro de la función acumuladora.
stream = Stream.scan(fn, accumulator, stream)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
fn | (accumulator, value) -> result |SKIP | Sí | Una función que toma un parámetro acumulador y valor y devuelve un nuevo valor acumulador del mismo tipo. |
accumulator | any | Sí | El valor inicial para el acumulador. |
stream | Stream | Sí | Stream que contiene los valores. |
devuelve | Stream | Devuelve un nuevo stream que contiene el resultado. |
Stream.scanMerge
Toma un array de pares de streams y funciones de escaneo y fusiona todos esos streams usando las funciones dadas en un solo stream.
stream = Stream.scanMerge(pairs, accumulator)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Sí | Un array de tuplas de stream y funciones de escaneo. |
accumulator | any | Sí | El valor inicial para el acumulador. |
devuelve | Stream | Devuelve un nuevo stream que contiene el resultado. |
Stream.lift
Crea un stream calculado que se actualiza reactivamente si alguno de sus streams ascendentes se actualiza. Ver la sección Combinación de streams. A diferencia de combine
, los streams de entrada son un número variable de argumentos (en lugar de un array) y la función callback recibe los valores del stream en lugar de los streams. No hay parámetro changed
. Esta función suele ser más fácil de usar en aplicaciones que combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
lifter | (any...) -> any | Sí | Ver argumento lifter. |
streams... | lista de Streams | Sí | Streams a elevar. |
devuelve | Stream | Devuelve un nuevo stream. |
lifter
Especifica cómo se genera el valor de un stream calculado. Ver la sección Combinación de streams.
any = lifter(streams...)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
streams... | splat de Streams | No | Splat de cero o más valores que corresponden a los valores de los streams pasados a stream.lift . |
devuelve | any | Devuelve un valor calculado. |
Stream.SKIP
Un valor especial que se puede devolver a las funciones callback del stream para omitir la ejecución de los streams descendentes.
Stream["fantasy-land/of"]
Este método es funcionalmente idéntico a stream
. Existe para cumplir con la especificación Applicative de Fantasy Land. Para obtener más información, consulta la sección ¿Qué es Fantasy Land?.
stream = Stream["fantasy-land/of"](value)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
value | any | No | Si este argumento está presente, el valor del stream se establece en él. |
devuelve | Stream | Devuelve un nuevo stream. |
Miembros de instancia
stream.map
Crea un stream dependiente cuyo valor se establece en el resultado de la función callback. Este método es un alias de stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
callback | any -> any | Sí | Una función callback cuyo valor de retorno se convierte en el valor del stream. |
devuelve | Stream | Devuelve un nuevo stream. |
stream.end
Un stream codependiente que anula el registro de los streams dependientes cuando se establece en true. Ver estado finalizado.
endStream = stream().end
stream["fantasy-land/of"]
Este método es funcionalmente idéntico a stream
. Existe para cumplir con la especificación Applicative de Fantasy Land. Para obtener más información, consulta la sección ¿Qué es Fantasy Land?.
stream = stream()["fantasy-land/of"](value)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
value | any | No | Si este argumento está presente, el valor del stream se establece en él. |
devuelve | Stream | Devuelve un nuevo stream. |
stream["fantasy-land/map"]
Crea un stream dependiente cuyo valor se establece en el resultado de la función callback. Ver encadenamiento de streams.
Este método existe para cumplir con la especificación Applicative de Fantasy Land. Para obtener más información, consulta la sección ¿Qué es Fantasy Land?.
dependentStream = stream()["fantasy-land/map"](callback)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
callback | any -> any | Sí | Una función callback cuyo valor de retorno se convierte en el valor del stream. |
devuelve | Stream | Devuelve un nuevo stream. |
stream["fantasy-land/ap"]
El nombre de este método significa apply
(aplicar). Si un stream a
tiene una función como su valor, otro stream b
puede usarlo como el argumento para b.ap(a)
. Llamar a ap
llamará a la función con el valor del stream b
como su argumento, y devolverá otro stream cuyo valor es el resultado de la llamada a la función. Este método existe para cumplir con la especificación Applicative de Fantasy Land. Para obtener más información, consulta la sección ¿Qué es Fantasy Land?.
stream = stream()["fantasy-land/ap"](apply)
Argumento | Tipo | Requerido | Descripción |
---|---|---|---|
apply | Stream | Sí | Un stream cuyo valor es una función. |
devuelve | Stream | Devuelve un nuevo stream. |
Uso básico
Los streams no forman parte de la distribución principal de Mithril.js. Para incluirlos en un proyecto, requiere su módulo:
var stream = require('mithril/stream');
Streams como variables
stream()
devuelve un stream. En su nivel más básico, un stream funciona de manera similar a una variable o una propiedad getter-setter: puede contener estado, que se puede modificar.
var username = stream('John');
console.log(username()); // registra "John"
username('John Doe');
console.log(username()); // registra "John Doe"
La principal diferencia es que un stream es una función y, por lo tanto, se puede componer en funciones de orden superior.
var users = stream();
// solicita usuarios de un servidor usando la API fetch
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
En el ejemplo anterior, el stream users
se completa con los datos de respuesta cuando se resuelve la solicitud.
Enlaces bidireccionales
Los streams también se pueden alimentar con datos provenientes de funciones callback de eventos, entre otros.
// un stream
var user = stream('');
// un enlace bidireccional al stream
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
En el ejemplo anterior, cuando el usuario escribe en la entrada, el stream user
se actualiza al valor del campo de entrada.
Propiedades calculadas
Los streams son útiles para implementar propiedades calculadas:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // registra "hello-world"
En el ejemplo anterior, el valor de slug
se calcula cuando se actualiza title
, no cuando se lee slug
.
Por supuesto, también es posible calcular propiedades basadas en múltiples 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"
Las propiedades calculadas en Mithril.js se actualizan de forma atómica: los streams que dependen de múltiples streams nunca se llamarán más de una vez por actualización de valor, sin importar cuán complejo sea el gráfico de dependencia de la propiedad calculada.
Encadenamiento de streams
Los streams pueden encadenarse mediante el método map
. Un stream encadenado también se conoce como stream dependiente.
// stream padre
var value = stream(1);
// stream dependiente
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // registra 2
Los streams dependientes son reactivos: sus valores se actualizan cada vez que se actualiza el valor de su stream padre. Esto ocurre tanto si el stream dependiente se creó antes como después de que se estableciera el valor del stream padre.
Puedes evitar que los streams dependientes se actualicen devolviendo el valor especial stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// nunca se ejecuta
});
Combinación de streams
Un stream puede depender de varios streams padres. Este tipo de streams se pueden crear a través de 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"
O puedes usar la función 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"
También hay un método de nivel inferior llamado stream.combine()
que expone los propios streams en los cálculos reactivos para casos de uso más avanzados.
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
Un stream puede depender de cualquier número de streams y se garantiza que se actualizará de forma atómica. Por ejemplo, si un stream A tiene dos streams dependientes B y C, y un cuarto stream D depende tanto de B como de C, el stream D solo se actualizará una vez si cambia el valor de A. Esto garantiza que la función callback para el stream D nunca se llame con valores inestables, como cuando B tiene un nuevo valor pero C tiene el valor anterior. La atomicidad también trae los beneficios de rendimiento de no volver a calcular los streams descendentes innecesariamente.
Puedes evitar que los streams dependientes se actualicen devolviendo el valor especial stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// nunca se ejecuta
});
Estados del stream
En un momento dado, un stream puede encontrarse en uno de tres estados: pendiente, activo y finalizado.
Estado pendiente
Los streams pendientes se pueden crear llamando a stream()
sin parámetros.
var pending = stream();
Si un stream depende de más de un stream y alguno de sus streams padres está en estado pendiente, el stream dependiente también está en estado pendiente y no actualiza su valor.
var a = stream(5);
var b = stream(); // stream pendiente
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // registra undefined
En el ejemplo anterior, added
es un stream pendiente, porque su padre b
también está pendiente.
Esto también se aplica a los streams dependientes creados a través de stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // registra undefined porque `doubled` está pendiente
Estado activo
Cuando un stream recibe un valor, se vuelve activo (a menos que el stream esté finalizado).
var stream1 = stream('hello'); // stream1 está activo
var stream2 = stream(); // stream2 comienza pendiente
stream2('world'); // luego se vuelve activo
Un stream dependiente con múltiples padres se vuelve activo si todos sus padres están activos.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
En el ejemplo anterior, el stream a
está activo, pero b
está pendiente. Establecer b("world")
haría que b
se volviera activo y, por lo tanto, greeting
también se volvería activo y se actualizaría para tener el valor "hello world"
.
Estado finalizado
Un stream puede dejar de influir en sus streams dependientes llamando a stream.end(true)
. Esto elimina efectivamente la conexión entre un stream y sus streams dependientes.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // establecer en estado finalizado
value(5);
console.log(doubled());
// registra undefined porque `doubled` ya no depende de `value`
Los streams finalizados mantienen su funcionamiento como contenedores de estado, es decir, todavía puedes usarlos como getter-setters, incluso después de que hayan finalizado.
var value = stream(1);
value.end(true); // establecer en estado finalizado
console.log(value(1)); // registra 1
value(2);
console.log(value()); // registra 2
Finalizar un stream es útil cuando tiene un ciclo de vida limitado (por ejemplo, reaccionar a los eventos mousemove
solo mientras se arrastra un elemento DOM, pero no después de que se haya soltado).
Serialización de streams
Los streams implementan un método .toJSON()
. Cuando un stream se pasa como argumento a JSON.stringify()
, se serializa el valor del stream.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // registra 123
Los streams no activan el renderizado
A diferencia de bibliotecas como Knockout, los streams de Mithril.js no activan el renderizado de plantillas. La repintura ocurre en respuesta a los controladores de eventos definidos en las vistas de componentes de Mithril.js, los cambios de ruta o después de que se resuelven las llamadas a m.request
.
Si se desea el redibujo en respuesta a otros eventos asíncronos (por ejemplo, setTimeout
/setInterval
, suscripción a websocket, controlador de eventos de biblioteca de terceros, etc.), debes llamar manualmente a m.redraw()
.
¿Qué es Fantasy Land?
Fantasy Land especifica la interoperabilidad de estructuras algebraicas comunes. En términos sencillos, significa que las bibliotecas que cumplen con las especificaciones de Fantasy Land se pueden usar para escribir código genérico de estilo funcional, independientemente de cómo implementen las construcciones internamente.
Por ejemplo, digamos que queremos crear una función genérica llamada plusOne
. La implementación simple se vería así:
function plusOne(a) {
return a + 1;
}
El problema con esta implementación es que solo se puede usar con un número. Sin embargo, es posible que cualquier lógica que produzca un valor para a
también pueda producir un estado de error (envuelto en un Maybe o un Either de una biblioteca como Sanctuary o Ramda-Fantasy), o podría ser un stream de Mithril.js, un stream de Flyd, etc. Idealmente, no querríamos escribir una versión similar de la misma función para cada tipo posible que a
podría tener y no querríamos estar escribiendo código de envoltura/desenvoltura/manejo de errores repetidamente.
Aquí es donde Fantasy Land puede ayudar. Reescribamos esa función en términos de un álgebra de Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Ahora este método funciona con cualquier Functor compatible con Fantasy Land, como R.Maybe
, S.Either
, stream
, etc.
Este ejemplo puede parecer complicado, pero es una compensación en complejidad: la implementación simple de plusOne
tiene sentido si tienes un sistema simple y solo incrementas números, pero la implementación de Fantasy Land se vuelve más poderosa si tienes un sistema grande con muchas abstracciones de envoltura y algoritmos reutilizados.
Al decidir si debes adoptar Fantasy Land, debes considerar la familiaridad de tu equipo con la programación funcional y ser realista con respecto al nivel de disciplina que tu equipo puede comprometerse a mantener la calidad del código (frente a la presión de escribir nuevas características y cumplir con los plazos). La programación funcional depende en gran medida de la creación, organización y dominio de un conjunto amplio de funciones pequeñas y bien definidas. Por lo tanto, no es recomendable para equipos con documentación deficiente o poca experiencia en lenguajes funcionales.