(번역)Practical Guide to Fp-ts P2: Option, Map, Flatten, Chain

김형태·2024년 2월 1일
0

fp-ts

목록 보기
2/4

원문: Practical Guide to Fp-ts P2: Option, Map, Flatten, Chain - Ryan's Blog

Introduction

이 글은 제 '실용적으로 fp-ts 배우기' 시리즈의 두 번째 글입니다. 첫 번째 글에서는 fp-ts의 구성요소인 pipeflow를 소개했습니다. 이 글에서는 Option 타입을 소개할 것입니다.

Options

Options는 undefined거나 null일 수 있는 값을 감싸는 컨테이너입니다. 값이 존재하는 경우에는 Option을 Some 타입이라 하고, undefined 또는 null인 경우 None 타입이라고 합니다.

fp-ts에서 Option 타입은 NoneSome의 판별 유니온(discriminated union)입니다.

type Option<A> = None | Some<A>

왜 처음으로 Option 타입을 사용해야하는 것일까요? 타입스크립트는 이미 undefinednull을 처리하는 좋은 방법이 있습니다. 예를 들면, 우리는 optional chaining이나 nullish coalescing을 사용할 수 있습니다.

Option 타입은 우리에게 강력한 도구를 제공합니다. 첫 번째 강력한 도구는 map 연산자입니다.

Map

map 연산자는 특정한 값을 다른 값으로 변환하거나 직관적으로 매핑할 수 있게 합니다. pipe 연산자와 익명함수를 이용하는 map 함수 예시입니다.

const foo = {
  bar: 'hello',
}

pipe(foo, (f) => f.bar) // hello

이 예시에서, foofoo.bar로 매핑되고, 우리는 hello라는 결과를 얻습니다. 이번에는 fooundefined일 수도 있는 경우, optional chaining을 사용하는 것으로 확장해보겠습니다.

interface Foo {
  bar: string
}

const foo = {
  bar: 'hello',
} as Foo | undefined

pipe(foo, (f) => f?.bar) // hello

예상대로hello를 얻었습니다. 하지만 더 나은 방법이 있습니다. 익명함수 내에서 명명된 변수 f가 있습니다. 일반적으로, 이를 피하는 것이 좋습니다. 외부 변수를 가려서(shadowing) 오류의 위험성이 있기 때문입니다. 다른 이유는 변수 네이밍의 어려움입니다. fnullableFoo와 같이 네이밍할 수도 있습니다. 결국, 이 변수를 위한 좋은 이름이 없습니다.

이 문제를 해결하기 위해 구조분해 할당을 사용해봅시다.

pipe(foo, ({ bar }) => bar) // Property 'bar' does not exist on type 'Foo | undefined'.ts (2339)

Property 'bar' does not exist on type 'Foo | undefined'.ts (2339)

이런.. 컴파일러는 undefined가 될 수도 있는 객체는 분해할 수 없다고 합니다.

Option 타입을 사용해봅시다.

pipe(
  foo,
  O.fromNullable,
  O.map(({ bar }) => bar),
) // { _tag: 'Some', value: 'hello' }
pipe(
  undefined,
  O.fromNullable,
  O.map(({ bar }) => bar),
) // { _tag: 'None' }

익명 함수를 Option 모듈의 map 함수로 대체한 후에는 더 이상 컴파일러에서 에러가 발생하지 않습니다. 이는 왜 그럴까요?

두 파이프의 출력을 살펴보겠습니다. 첫 번째 파이프에서는 { _tag: 'Some', value: 'hello' }가 출력됩니다. 이는 원래 출력인 'hello'와 비교됩니다. 마찬가지로 두 번째 파이프에서는 undefined 대신 { _tag: 'None' }이 출력됩니다.

직관적으로 이는 우리의 map 함수가 'hello' 또는 undefined와 같은 원시값을 대상으로 작동하는 것이 아니라, 컨테이너 객체 상에서 작동하는 것을 의미합니다.

파이프 함수의 두 번째 연산인 O.fromNullable은 이 컨테이너 객체를 생성합니다. 이 함수는 Option이 Some인지 None인지를 구별할 수 있게 하는 _tag 속성을 추가하여 nullable한 값을 컨테이너로 들어올립니다. _tagNone이면 value 프로퍼티는 삭제됩니다.

다시 map 함수로 돌아가봅시다. O.map은 어떻게 Option 컨테이너 상에서 작동할까요? 이 함수는 _tag 속성을 기반으로 비교를 수행합니다. 만약 _tagSome이라면, map에 전달된 함수를 사용하여 값을 변환합니다. 이 경우에는 ({ bar }) => bar를 사용했습니다. 그러나_tagNone이면 어떤 작업도 수행되지 않습니다. 컨테이너는 여전히 None 상태를 유지합니다.

Flatten

객체가 순차적으로 중첩된 nullable 프로퍼티들을 갖는 상황은 어떻게 처리할까요? 위에서 다룬 예제를 확장해 보겠습니다.

interface Fizz {
  buzz: string
}

interface Foo {
  bar?: Fizz
}

const foo = { bar: undefined } as Foo | undefined

pipe(foo, (f) => f?.bar?.buzz) // undefined

optional chaining을 사용해 동작하게 만들려면, 물음표만 추가하면 됩니다. Option 타입을 사용하면 어떻게 될까요?

pipe(
  foo,
  O.fromNullable,
  O.map(({ bar: { buzz } }) => buzz),
)

Property 'buzz' does not exist on type 'Fizz | undefined'.ts (2339)

안타깝게도, 우리가 이전에 접했던 문제를 다시 보게 됩니다. undefined가 될 수도 있는 객체는 구조 분해할 수 없는 것이죠.

우리가 할 수 있는 건 O.fromNullable을 사용해 foobar를 Option 타입으로 들어올리는 것입니다.

pipe(
  foo,
  O.fromNullable,
  O.map(({ bar }) =>
    pipe(
      bar,
      O.fromNullable,
      O.map(({ buzz }) => buzz),
    ),
  ),
) // { _tag: 'Some', value: { _tag: 'None' } }

하지만 이제 두 가지 새로운 문제가 생겼습니다. 첫째로, 굉장히 장황합니다. 둘째로, 중첩된 Option이 있습니다. 바깥 Option과 안쪽 Option을 보세요. 전자는 Some으로, foo.bar가 정의되어 있으므로 기대한 대로입니다. 후자는 foo.bar.buzzundefined이므로 None입니다. 최종 Option의 결과에만 관심이 있다면 매번 Option의 중첩된 태그 목록을 탐색해야 합니다.

최종 Option에만 관심이 있다면, 중첩된 Option을 하나의 Option으로 평탄화(flatten)할 수 있을까요?

O.flatten 연산자를 소개하겠습니다.

pipe(
  foo,
  O.fromNullable,
  O.map(({ bar }) =>
    pipe(
      bar,
      O.fromNullable,
      O.map(({ buzz }) => buzz),
    ),
  ),
  O.flatten,
) // { _tag: 'None' }

이제 우리는 파이프라인의 마지막 Option인 { _tag: 'None'} 하나만 가지고 있습니다. 이 Option이 Some인지 None인지 확인하려면, 결과를 O.isSome 또는 O.isNone으로 파이프로 연결할 수 있습니다.

하지만 여전히 장황하다는 문제가 있습니다. 중첩된 옵션을 한 번에 매핑하고 평탄화할 수 있는 단일 연산자가 있으면 유용할 것입니다. 직관적으로, 이를 종종flatmap 연산자라고 합니다.

Chain(Flatmap)

fp-ts에서는 flatmap 연산자를 chain이라고 합니다. 위 코드를 다음과 같이 리팩터링할 수 있습니다.

pipe(
  foo,
  O.fromNullable,
  O.map(({ bar }) => bar),
  O.chain(
    flow(
      O.fromNullable,
      O.map(({ buzz }) => buzz),
    ),
  ),
) // { _tag: 'None' }

더 적은 코드로 동일한 결과를 얻을 수 있습니다.

Conclusion

대부분의 경우에는 Option을 사용할 필요가 없을 것이며, optional chaining이 더 간결합니다. 그러나 Option 타입은 단순히 null을 확인하는 데 그치지 않습니다. Option은 실패하는 작업을 나타내는 데 사용할 수 있습니다. undefined를 Option으로 들어올릴 수 있는 것처럼, Option을 다른 fp-ts 컨테이너로 들어올릴 수도 있습니다. 예를 들어, Either와 같은 컨테이너입니다.

더 많은 정보를 얻으시려면, Option 공식 문서를 확인하세요.

profile
steady

0개의 댓글