타입 있는 에러

타입 있는 에러로 실패하는 일직선 코드를 작성하세요. Kotlin Arrow 2.x의 접근 방식을 Dart로 이식했습니다.

Either<E, A> either<E, A>(A Function(Raise<E> r) block) Future<Either<E, A>> eitherAsync<E, A>(FutureOr<A> Function(Raise<E> r) block) A? nullable<A>(A Function(SingletonRaise r) block)
깊이 알아보기. 이 페이지는 전체 개요입니다. 각 주제마다 실행 가능한 데모가 담긴 상세 튜토리얼이 있습니다: Either · either & Raise 스코프 · nullable · NonEmptyList · 에러 누적 · Either × 파이프라인

Kotlin Arrow에서 Dart로

Kotlin Arrow의 either { } 블록은 실패할 수 있는 단계들의 연쇄를 일직선 코드로 바꿔 줍니다 — 각 .bind()는 성공 값을 풀어내거나, 실패와 함께 블록 전체를 단락시킵니다:

// Kotlin Arrow
fun getResult(): Either<Failure, SuccessData> = either {
    val user  = findUser(userId).bind()
    val order = findOrder(user.id).bind()
    val total = calculateTotal(order).bind()
    SuccessData(user, order, total)
}

FxDart는 Dart에서 같은 모양을 제공합니다:

// FxDart
Either<Failure, SuccessData> getResult() => either((r) {
  final user  = r.bind(findUser(userId));
  final order = r.bind(findOrder(user.id));
  final total = r.bind(calculateTotal(order));
  return SuccessData(user, order, total);
});

둘 다, 이렇게 쓸 수밖에 없었던 중첩 flatMap 피라미드를 대체합니다:

// 이것을 대체합니다
Either<Failure, SuccessData> getResult() =>
    findUser(userId).flatMap((user) =>
        findOrder(user.id).flatMap((order) =>
            calculateTotal(order).map((total) =>
                SuccessData(user, order, total))));

Kotlin과의 차이 두 가지는 Dart의 현실입니다. Dart에는 람다 리시버가 없으므로 스코프가 명시적 매개변수(r)로 전달되고, inline이 없으므로 비동기는 별도의 빌더 (eitherAsync)를 사용합니다. 내부 구현은 flatMap 연쇄가 아닙니다. Arrow와 마찬가지로, 실패한 r.bind는 스코프 토큰이 달린 비공개 신호를 던지고 빌더가 경계에서 잡아냅니다 — 그래서 이른 반환, 반복문, if가 블록 안에서 전부 그대로 동작하고, 중첩된 빌더가 서로의 에러를 가로채는 일도 없습니다.

깊이 알아보기: Either

스코프 어휘

모든 것이 빌더가 건네주는 r에 달려 있습니다 — r.을 입력하면 전부 발견할 수 있습니다:

Either<String, int> parsePort(String raw) => either((r) {
  final n = r.ensureNotNull(int.tryParse(raw), () => '"$raw"는 숫자가 아닙니다');
  r.ensure(n > 0 && n < 65536, () => '$n은 범위를 벗어났습니다');
  return n;
});

switch (parsePort('8080')) {
  case Right(:final value): print('$value 포트에서 대기 중');
  case Left(:final value):  print('잘못된 설정: $value');
}

eitherAsync는 비동기 쌍둥이이고(raise는 같은 await 체인 안에서만), nullable/nullableAsyncEither 대신 T?를 돌려주는 nullable 우선 쌍둥이입니다 — FxDart는 nullable 우선이므로 Option 타입은 없습니다.

깊이 알아보기: either & Raise 스코프 → · 깊이 알아보기: nullable

첫 실패만이 아니라 모든 실패를 모으기

검증에는 첫 번째 에러가 아니라 모든 에러가 필요합니다. 별도의 Validated 타입을 대체하는 Arrow의 방식입니다:

final user = either<Nel<String>, User>((r) => r.accumulate((acc) {
  final name = acc.accumulating((r) => validateName(r, input));
  final age  = acc.accumulating((r) => validateAge(r, input));
  return User(name.value, age.value); // 모든 에러가 한 번에 보고됩니다
}));

r.accumulate는 모든 분기를 실행하고 실패 전부를 NonEmptyList(Nel)로 이어 붙입니다 — 비어 있을 수 없는 제로 비용 확장 타입입니다. 고정 인자 편의 함수 r.zipOrAccumulate2..5가 흔한 경우를 담당하고, r.mapOrAccumulate(items, transform)는 컬렉션 전체를 fail-slow로 검증합니다. r.bindNel은 한 분기가 여러 에러를 한꺼번에 보태게 해 주고, someEither.toEitherNel()은 fail-fast 값을 누적 스코프로 이어 줍니다.

깊이 알아보기: 에러 누적 → · 깊이 알아보기: NonEmptyList

파이프라인과의 융합

Arrow에도, 다른 Dart FP 라이브러리에도 없는 부분입니다. 타입 있는 에러가 FxDart의 지연·동시성 파이프라인과 융합됩니다.

// 레코드 500건을 한 번에 8건씩 검증하고, 모든 실패를 순서대로 보존합니다.
final result = await fxStream(records)
    .mapOrAccumulate<String, User>((r, rec) async {
  final parsed = r.ensureNotNull(tryParse(rec), () => '잘못된 레코드: $rec');
  return await enrich(parsed);
}, concurrency: 8);

rights(), lefts(), separated(), sequence()(fail-fast — 첫 Left에서 상류 당기기를 멈춤), mapOrAccumulate()(fail-slow)는 fx()/비동기 체인의 즉시 실행 종결 연산자입니다. 동시성 검증은 FxDart의 다른 기능과 똑같이 concurrent(n) 역채널 위에서 동작하며, 각 원소는 자기만의 스코프에서 실행되므로 한 원소의 실패가 다른 원소로 새어 나갈 수 없습니다.

깊이 알아보기: Either × 파이프라인 →

예외 vs raise된 에러

경계는 단호합니다. raise된 에러는 도메인의 타입 있는 실패이고, throw된 예외는 결함이므로 either를 그대로 뚫고 전파됩니다. throw를 Either로 붙잡고 싶다면 명시적으로 쓰세요:

final parsed = Either.catching(() => jsonDecode(raw));       // Either<Object, dynamic>
final typed  = Either.catchingWith(ParseFailure.new, () => jsonDecode(raw));

깊이 알아보기: Either.catchingEither 페이지에 있습니다 →

규칙 두 가지. (1) raise 블록에서 지연 파이프라인을 그대로 반환하지 마세요 — toList()로 구체화하거나 위의 즉시 실행 종결 연산자를 사용하세요. 지연된 raise는 RaiseLeakedError로 요란하게 실패합니다. (2) raise 블록 안에서 맨몸 catch를 쓰지 마세요 — 단락 신호를 항상 통과시키는 catching/catchingAsync를 사용하세요 (신호는 Error이므로 on Exception은 이미 안전합니다).

왜 이 페이지의 이름이 타입 있는 에러일까요? Monad 같은 함수형 프로그래밍 용어를 쓰지 않고 말이죠. 이름에 담긴 이유 →