tee

Ejecuta varios folds sobre una pasada de la fuente: una iteración, nada almacenado.

typedef Fold<A, R> = ({R seed, R Function(R acc, A a) step}); (R1, R2) tee<A, R1, R2>(Iterable<A> iterable, Fold<A, R1> first, Fold<A, R2> second) (R1, R2, R3) tee3<A, R1, R2, R3>(Iterable<A> iterable, Fold<A, R1> first, Fold<A, R2> second, Fold<A, R3> third) Future<(R1, R2)> teeAsync<A, R1, R2>(FxAsyncIterable<A> iterable, AsyncFold<A, R1> first, AsyncFold<A, R2> second)

Lección

Dos preguntas sobre los mismos datos suelen costar dos pasadas: readings.fold(...) para el total y luego readings.reduce(...) para el pico. Eso está bien para una List, y está mal para cualquier otra cosa: un generador sync*, una página de red o una fuente que cuenta cuántas veces se ejecutó se recorrerán dos veces. tee hace las dos preguntas a la vez: cada elemento hace avanzar el total y el pico antes de tirar del siguiente, así que la fuente se recorre exactamente una vez.

Un lector se da como un fold: un record con seed (dónde empieza) y step (cómo lo hace avanzar un elemento). Esa forma es lo que hace gratis la pasada única. Como los dos lectores se mueven juntos, elemento a elemento, nunca hay un valor que uno haya visto y el otro no, así que no hay nada que recordar: tee sobre un millón de elementos sostiene dos acumuladores, no un millón de valores. Los dos acumuladores son totalmente independientes y no tienen por qué compartir tipo. tee3 admite tres.

La restricción es el precio de eso. tee alimenta folds, no pipelines: los lectores no pueden avanzar a su propio ritmo, tomar cantidades distintas ni pararse antes por su cuenta. Cuando de verdad necesitas dos lectores independientes, echa mano de fork y acepta el búfer compartido que mantiene para que un cursor rezagado pueda alcanzar al otro. Regla práctica: si los dos lectores consumen la fuente entera y la reducen a un valor, tee; si alguno es un pipeline por derecho propio, fork.

De dónde viene el nombre

tee no es una abreviatura: es la letra T, tomada de la pieza en T de la fontanería. Un empalme en T divide una tubería en dos, así que lo que entra por un lado sale por dos a la vez. Unix se quedó con la imagen para su orden tee, que lee la entrada estándar y la envía a la salida estándar y a un fichero al mismo tiempo:

       input
        │
        ▼
    ┌───┴───┐
    │  tee  │
    └───┬───┘
   ┌────┴────┐
   ▼         ▼
  stdout    file

El itertools.tee() de Python toma prestada la misma imagen, dividiendo un iterable en varios iteradores independientes. Conviene saberlo, porque esa es la parte que FxDart llama fork, no tee: fork te da cursores independientes, como el de Python. El tee de FxDart ramifica el consumo en su lugar: una pasada, varios folds leyéndola al unísono. La misma imagen en forma de T, dividida un nivel más aguas abajo.

Demo 1 · Total y pico con una sola lectura

sensor() incrementa reads por cada valor que produce. Dos pasadas separadas dejarían reads en 12; tee lo deja en 6:

Demo 2 · Acumuladores independientes, tee3, y sobre una cadena

Los dos folds llevan tipos que no tienen nada que ver: un recuento de caracteres int junto a un String con el ganador provisional. tee3 añade un tercer fold y, sobre una cadena fx, los folds ven lo que produce la cadena, no la fuente original:

Pruébalo tú

Ejercicio: ahora mismo sensor() se recorre dos veces, así que reads imprime 6. Sustituye las dos pasadas por un solo tee — sumando en un fold y contando en el otro — para que reads imprima 3.

Relacionado: fork — lectores independientes, a costa de un búfer · reduce — un solo fold · groupBy — muchos acumuladores indexados por valor