fx

Envuelve una secuencia en un pipeline perezoso y encadenable: el corazón tipado de FxDart.

Fx<T> fx<T>(Iterable<T> iterable) FxAsync<T> fxAsync<T>(FxAsyncIterable<T> iterable) FxAsync<T> fxStream<T>(Stream<T> stream) FxEvents<T> fxEvents<T>(Stream<T> stream) Fx<T> Iterable<T>.fx FxAsync<T> FxAsyncIterable<T>.fx FxAsync<T> Stream<T>.fx FxAsync<T> Iterable<FutureOr<T>>.fxAsync FxEvents<T> Stream<T>.fxEvents

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 terminaltoList(), 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.

Cambio incompatible en 0.8.0: 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.

ReceptorGetterEquivale a
Iterable<T>.fxfx(xs)
FxAsyncIterable<T>.fxfxAsync(it)
Stream<T>.fxfxStream(s)
Iterable<FutureOr<T>>.fxAsynctoAsync(xs)
Stream<T>.fxEventsfxEvents(s)
Stream<T>.fxLiveLiveValue.from(s)
Stream<T>.fxLiveSeededLiveValue.seededFrom(v, s)
Iterable<T>.fxShuffleshuffle(xs)
FxAsyncIterable<T>.fxShuffleshuffleAsync(it)
void Function(T).fxDebouncedebounce(f, w)
void Function(T).fxThrottlethrottle(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.

Relacionado: pipe — la alternativa de tipado dinámico · toList — el operador terminal más habitual · each — operador terminal para efectos secundarios · consume — operador terminal que descarta los resultados