fx

시퀀스를 지연 평가되는 체인 가능한 파이프라인으로 감쌉니다 — 타입이 살아 있는 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

강의

이 코스의 모든 내용은 결국 하나의 개념, 체인으로 모입니다. fx(iterable)은 임의의 Iterable<T>Fx<T>로 감쌉니다 — .map(), .filter(), .take()처럼 FxTS 스타일의 메서드가 달려 있는 객체입니다. 이 호출들은 하나같이 지연 계산을 조금씩 더 얹은 새로운 Fx를 반환할 뿐, 아직 아무것도 실행하지 않습니다. Fx가 실제로 일을 시작하는 시점은 종결 연산자를 호출할 때입니다 — toList(), each(), consume(), reduce() 등이 체인 전체를 통해 값을 하나씩, 종결 지점에서 소스까지 거슬러 끌어당깁니다.

이 지연 평가 덕분에 FxDart는 아주 크거나 무한한 시퀀스 (range, cycle, repeat) 위에서도 안전하게 체인을 이어 갈 수 있습니다. 하류의 무언가가 — 보통은 take(n)이 — 실제로 몇 개를 끌어당길지 정해 주기만 하면, 상류 단계는 딱 그만큼만 실행됩니다.

fx는 체인의 동기 쪽 절반입니다. 비동기 짝으로는 FxAsyncIterable(toAsync, fromStream, 혹은 *Async 계열 함수에서 얻는 것)을 감싸는 fxAsync, 그리고 Dart의 Stream을 바로 감싸는 단축 함수 fxStream이 있습니다. 둘 다 FxAsync<T> 체인을 반환하며, 이 체인의 메서드는 Future를 반환하는 함수도 받아들이고, 종결 연산자는 모두 await할 수 있는 Future를 반환합니다. 체인 중간에 동기에서 비동기로 넘어가려면 .toAsync()를 쓰면 됩니다.

그냥 map(f, iterable) 같은 최상위 함수를 호출하면 될 텐데 왜 이런 게 필요할까요? Dart로는 FxTS의 TypeScript처럼 가변 인자 pipe에 타입을 붙일 수 없기 때문입니다(다음 강의 참고). fx() 체이닝은 그 대신 FxDart가 완전한 타입과 자동 완성이 살아 있는 파이프라인을 얻는 방법입니다.

0.8.0 파괴적 변경: Fx<T>는 이제 확장 타입이며, 런타임에는 감싸고 있던 Iterable<T>로 소거됩니다. 문서화된 API는 모두 그대로이고 체인도 이전과 똑같이 동작합니다. 깨지는 것은 x is Fx<T> 검사(런타임에 그 타입이 존재하지 않습니다), 그리고 Fx를 직접 extend하거나 implement하려던 코드입니다(대신 최상위 함수를 쓰세요). fx()를 평범하게 쓰고 있다면 고칠 것은 없습니다.

데모 1 · 종결 연산자 전까지는 아무것도 실행되지 않습니다

체인을 만든 직후에는 calls가 0에 머물러 있다가, toList()가 실제로 값 5개를 끌어당기는 순간 올라가는 것을 보세요.

데모 2 · fxAsync와 fxStream

fxAsyncFxAsyncIterable(여기서는 toAsync로 만든 것)을 감싸고, fxStreamStream을 바로 감쌉니다. 둘 다 같은 체인 메서드를 비동기 버전으로 제공합니다.

getter 표기

모든 진입점에는 getter 형태도 있습니다. Iterable, FxAsyncIterable, Stream에는 .fx가, Future의 이터러블에는 .fxAsync가, Stream에는 .fxEvents가 붙습니다. 만들어지는 체인은 완전히 같고, 차이는 표현식을 어느 쪽부터 읽느냐뿐입니다.

// 함수 표기: 괄호를 열려면 표현식 앞으로 되돌아가야 합니다
fx(orders.where(isPaid)).groupBy((o) => o.customerId);

// getter 표기: .toList()를 읽듯 왼쪽에서 오른쪽으로
orders.where(isPaid).fx.groupBy((o) => o.customerId);

이쪽이 Dart의 관용 표기이고, 비용은 없습니다. Fx는 extension type이라 래퍼가 이터러블 자체로 소거되고, 본문이 this뿐인 getter는 컴파일러가 지워 버리는 정적 호출입니다. 100만 원소의 map + filter + sum에서 두 표기는 12.640 ms와 12.665 ms — 같은 숫자가 두 번 나옵니다.

이 문서는 전부 fx()로 씁니다. FxTS가 쓰는 이름이고, 타입 인자를 명시할 수 있는 쪽이기 때문입니다. getter는 후위로 타입 인자를 받을 수 없어서 fx<num>(xs)는 되지만 xs.fx<num>은 파싱되지 않습니다. 실제 코드에서는 더 잘 읽히는 쪽을 쓰세요 — 컴파일 결과는 동일합니다.

한 가지 비대칭은 알아 둘 만합니다. Iterable<Future<T>>.fx를 붙이면 Fx<Future<T>>가 됩니다 — 값이 아니라 Future 자체를 훑는 체인이고, 컴파일은 되면서 조용히 엉뚱하게 동작합니다. .fxAsync가 그래서 있습니다. Future를 await해 주므로 T는 결과 타입이 되고, concurrent(n)이 일할 거리를 갖게 됩니다.

await responses.fxAsync.map(parse).concurrent(4).toList();

Stream에는 두 getter가 모두 붙습니다. 양쪽 세계에 걸쳐 있는 유일한 소스이기 때문입니다. .fxpull 체인으로, fxStream과 같습니다. .fxEventspush 체인으로, fxEvents와 같습니다 — debounce, throttle, switch가 여기에 있습니다. push에서 pull로 건너올 때는 .pull()을 씁니다. 둘을 나란히 놓고 비교한 표는 Stream 다리에 있습니다.

keystrokes.fxEvents
    .debounce(const Duration(milliseconds: 160))
    .switchMap((q) => search(q).asStream())
    .pull()
    .toList();

getter 표기 전체

규칙은 하나입니다. 진입점 이름에는 fx가 들어갑니다. 어느 라이브러리로 들어가는지를 밝혀 주고, 맨 이름 — toAsync, shuffle, debounce — 은 프로젝트가 그 타입에 붙일 몫으로 남겨 둡니다.

수신 타입getter같은 것
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)

연산자 자체는 이 목록에 없고, 앞으로도 넣지 않습니다. 그중 열다섯 개 — map, where, take, fold 등 — 은 Iterable이 이미 가진 멤버와 이름이 같고, 인스턴스 멤버는 항상 확장을 이기므로 그 호출은 fxdart에 닿을 수조차 없습니다. 연산자가 사는 곳은 체인입니다 — xs.map(f)가 아니라 xs.fx.map(f)입니다.

직접 해 보기

연습: 60점 이상인 점수만 남기고, 보너스 점수로 두 배를 한 뒤, 앞의 2개 결과만 가져오는 체인을 만들어 보세요.

관련 항목: pipe — 동적 타입 방식의 대안입니다 · toList — 가장 흔히 쓰는 종결 연산자입니다 · each — 부수 효과를 위한 종결 연산자입니다 · consume — 결과를 버리는 종결 연산자입니다