tee

한 번의 순회로 여러 개의 폴드를 굴립니다 — 반복은 한 번, 버퍼는 없음.

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)

강의

같은 데이터에 대한 두 개의 질문은 보통 두 번의 순회를 요구합니다. 합계는 readings.fold(...)로, 최댓값은 readings.reduce(...)로. List라면 괜찮지만, 그 외의 것에는 잘못된 방식입니다 — sync* 제너레이터, 네트워크 페이지, 몇 번 실행됐는지 세는 소스는 모두 두 번 순회됩니다. tee는 두 질문을 한꺼번에 던집니다. 다음 원소를 당기기 전에 각 원소가 합계 최댓값을 모두 전진시키므로, 소스는 정확히 한 번만 순회됩니다.

리더는 폴드로 주어집니다 — 어디서 시작하는지를 나타내는 seed와, 원소 하나가 그것을 어떻게 전진시키는지를 나타내는 step의 레코드입니다. 바로 이 모양이 한 번의 순회를 공짜로 만듭니다. 두 리더가 원소 단위로 함께 움직이므로, 한쪽은 봤는데 다른 쪽은 못 본 값이란 존재하지 않고, 따라서 기억해 둘 것도 없습니다. 원소가 백만 개여도 tee가 들고 있는 것은 백만 개의 값이 아니라 두 개의 누산기뿐입니다. 두 누산기는 완전히 독립적이며 타입이 같을 필요도 없습니다. tee3는 세 개를 받습니다.

그 대가가 제약입니다. tee가 먹이는 것은 폴드이지 파이프라인이 아닙니다 — 리더들은 각자의 속도로 전진하거나, 서로 다른 개수만 가져가거나, 따로 일찍 멈출 수 없습니다. 정말로 독립적인 리더 둘이 필요하다면 fork를 쓰고, 뒤처진 커서가 따라올 수 있도록 유지되는 공유 버퍼를 감수하세요. 기준은 이렇습니다: 두 리더가 모두 소스를 끝까지 소비해서 하나의 값으로 줄인다면 tee, 둘 중 하나라도 그 자체로 파이프라인이라면 fork.

이름의 유래

tee는 약어가 아니라 알파벳 T 자체입니다. 배관에서 쓰는 T-스플리터에서 온 이름이죠. T자 이음쇠는 파이프 하나를 둘로 갈라, 한 방향으로 흐르던 것이 동시에 두 방향으로 나가게 합니다. Unix는 이 이미지를 그대로 가져와 tee 명령어를 만들었습니다. 표준 입력을 읽어 표준 출력 파일로 동시에 보내죠:

       입력
        │
        ▼
    ┌───┴───┐
    │  tee  │
    └───┬───┘
   ┌────┴────┐
   ▼         ▼
  표준출력   파일

Python의 itertools.tee()도 같은 그림에서 이름을 빌려, 하나의 이터러블을 여러 개의 독립적인 이터레이터로 갈라 줍니다. 알아 둘 만한 점은, 그쪽이 바로 FxDart가 fork라고 부르는 것이지 tee가 아니라는 것입니다. fork는 Python의 것처럼 독립적인 커서를 주고, FxDart의 tee는 대신 소비를 가릅니다 — 한 번의 순회를 여러 폴드가 보폭을 맞춰 읽는 것이죠. 같은 T자 그림을, 한 단계 더 하류에서 가른 셈입니다.

데모 1 · 한 번 읽어서 얻는 합계와 최댓값

sensor()는 값을 내놓을 때마다 reads를 증가시킵니다. 따로 두 번 순회했다면 reads는 12가 되지만, tee는 6에서 멈춥니다:

데모 2 · 독립적인 누산기, tee3, 그리고 체인 위에서

두 폴드는 서로 무관한 타입을 나릅니다 — int 문자 수와 String 현재 우승자가 나란히 갑니다. tee3는 세 번째 폴드를 더하고, fx 체인 위에서 폴드들은 원래 소스가 아니라 체인이 만들어 내는 것을 봅니다:

직접 해보기

연습: 지금은 sensor()를 두 번 순회하고 있어서 reads가 6을 출력합니다. 두 번의 순회를 하나의 tee로 바꿔 — 한 폴드에서 합계를, 다른 폴드에서 개수를 세도록 — reads가 3을 출력하게 만드세요.

관련 항목: fork — 버퍼를 대가로 한 독립 리더 · reduce — 폴드 하나 · groupBy — 값을 키로 하는 여러 누산기