fx
Envuelve una secuencia en un pipeline perezoso y encadenable: el corazón tipado de FxDart.
Lección
Todo este curso apunta a una sola idea: la cadena.
fx(iterable) envuelve cualquier Iterable<T> en un
Fx<T>: un objeto con métodos al estilo de FxTS como
.map(), .filter() y .take()
colgando de él. Cada una de esas llamadas devuelve un nuevo
Fx que envuelve un poco más de cómputo perezoso. Nada se
ejecuta todavía. Fx solo empieza a trabajar cuando llamas a un
operador terminal — toList(),
each(), consume(), reduce() y
compañía —, que tira de los valores a través de toda la cadena, uno a uno,
desde el terminal hasta la fuente.
Esta pereza es lo que permite a FxDart encadenar sin riesgo sobre secuencias
enormes o infinitas (range, cycle, repeat):
mientras algo aguas abajo — normalmente take(n) — decida
cuántos valores pedir de verdad, los pasos de aguas arriba solo se
ejecutan esas veces.
fx es la mitad síncrona de la cadena. Sus equivalentes
asíncronos son fxAsync, que envuelve un
FxAsyncIterable (lo que obtienes de toAsync,
fromStream o cualquier función *Async), y
fxStream, un atajo que envuelve directamente un Stream
de Dart. Ambos devuelven una cadena FxAsync<T> cuyos
métodos aceptan funciones que pueden devolver un Future, y
cuyos operadores terminales devuelven todos un Future que
esperas con await. Pasa de síncrono a asíncrono a mitad de cadena con
.toAsync().
¿Y por qué existe esto, en lugar de llamar sin más a funciones de nivel
superior como map(f, iterable)? Porque Dart no puede tipar un
pipe variádico como sí hace TypeScript en FxTS (lo verás en la
siguiente lección): el encadenado con fx() es la forma que tiene
FxDart de ofrecer pipelines totalmente tipados y con autocompletado.
Fx<T> ahora es un extension type que en
tiempo de ejecución se borra al Iterable<T> que envuelve.
Todas las APIs documentadas siguen igual: las cadenas funcionan exactamente
como antes. Lo que se rompe: las comprobaciones
x is Fx<T> (el tipo no existe en tiempo de ejecución) y el
código que intentaba extender o implementar Fx directamente (usa
las funciones de nivel superior en su lugar). Si usas fx() de la
forma habitual, no tienes que cambiar nada.
Demo 1 · Nada se ejecuta hasta el operador terminal
Fíjate en que calls se queda en 0 justo después de construir la cadena, y
salta en cuanto toList() tira de verdad de los 5 valores:
Demo 2 · fxAsync y fxStream
fxAsync envuelve un FxAsyncIterable (aquí, desde
toAsync); fxStream envuelve un Stream
directamente. Ambos te dan los mismos métodos de cadena, en versión asíncrona:
La forma con getter
Cada punto de entrada existe también como getter: .fx sobre un
Iterable, un FxAsyncIterable o un
Stream, .fxAsync sobre un iterable de futuros y
.fxEvents sobre un Stream. Construyen exactamente
la misma cadena; lo único que cambia es por qué extremo de la expresión se
empieza a leer:
// la función: hay que volver al principio para abrir el paréntesis
fx(orders.where(isPaid)).groupBy((o) => o.customerId);
// el getter: de izquierda a derecha, como se lee .toList()
orders.where(isPaid).fx.groupBy((o) => o.customerId);
Esta es la forma idiomática en Dart, y es gratis: Fx es un
extension type, así que el envoltorio se borra y queda el iterable mismo, y
un getter cuyo cuerpo es this es una llamada estática que el
compilador elimina. Sobre un map + filter +
sum de un millón de elementos, ambas formas miden 12.640 ms y
12.665 ms — el mismo número dos veces.
Estas páginas usan fx() en todo momento. Es el
nombre que usa FxTS y es el que admite un argumento de tipo explícito, algo
que un getter no puede recibir en posición posfija:
fx<num>(xs) funciona donde
xs.fx<num> ni siquiera parsea. En tu propio código elige
la que mejor se lea; compilan a lo mismo.
Conviene conocer una asimetría. Sobre un
Iterable<Future<T>>, .fx devuelve un
Fx<Future<T>> — una cadena sobre los futuros y no
sobre sus valores, que compila y hace lo incorrecto en silencio. Para eso
está .fxAsync: los espera, de modo que T es el
tipo resuelto y concurrent(n) tiene algo que hacer.
await responses.fxAsync.map(parse).concurrent(4).toList();
Un Stream lleva ambos getters, porque es la única fuente que
pertenece a los dos mundos. .fx da la cadena pull, lo
mismo que fxStream; .fxEvents da la cadena
push, lo mismo que fxEvents —
debounce, throttle, switch. Para volver de push a pull,
.pull(). Se comparan lado a lado en
Puentes de Stream.
keystrokes.fxEvents
.debounce(const Duration(milliseconds: 160))
.switchMap((q) => search(q).asStream())
.pull()
.toList();
Todas las formas con getter
La convención es una sola regla: un punto de entrada lleva fx
en el nombre. Dice en qué librería estás entrando y deja el nombre desnudo
— toAsync, shuffle, debounce —
libre para lo que el proyecto quiera poner en ese tipo.
| Receptor | Getter | Equivale a |
|---|---|---|
Iterable<T> | .fx | fx(xs) |
FxAsyncIterable<T> | .fx | fxAsync(it) |
Stream<T> | .fx | fxStream(s) |
Iterable<FutureOr<T>> | .fxAsync | toAsync(xs) |
Stream<T> | .fxEvents | fxEvents(s) |
Stream<T> | .fxLive | LiveValue.from(s) |
Stream<T> | .fxLiveSeeded | LiveValue.seededFrom(v, s) |
Iterable<T> | .fxShuffle | shuffle(xs) |
FxAsyncIterable<T> | .fxShuffle | shuffleAsync(it) |
void Function(T) | .fxDebounce | debounce(f, w) |
void Function(T) | .fxThrottle | throttle(f, w) |
Los operadores no están en esta lista, y no lo estarán. Quince de ellos —
map, where, take,
fold y compañía — comparten nombre con un miembro que
Iterable ya tiene, y un miembro de instancia siempre gana a
una extensión, así que esas llamadas nunca podrían llegar a fxdart. Los
operadores viven en la cadena: xs.fx.map(f), no
xs.map(f).
Pruébalo tú
Ejercicio: construye una cadena que se quede con las puntuaciones de 60 o más, las duplique como puntos extra y tome solo los 2 primeros resultados.