중첩된 context 문제

일반적인 문제를 풀기 위해 무언가 더 필요한 이유를 보여주는 몇가지 예제를 살펴봅시다.

예제 (F = Array)

Follower 들의 follower 가 필요한 상황이라 가정합시다.

import { pipe } from 'fp-ts/function'
import * as A from 'fp-ts/ReadonlyArray'

interface User {
  readonly id: number
  readonly name: string
  readonly followers: ReadonlyArray<User>
}

const getFollowers = (user: User): ReadonlyArray<User> => user.followers

declare const user: User

// followersOfFollowers: ReadonlyArray<ReadonlyArray<User>>
const followersOfFollowers = pipe(user, getFollowers, A.map(getFollowers))

여기서 문제가 발생합니다, followersOfFollowers 의 타입은 ReadonlyArray<ReadonlyArray<User>> 이지만 우리가 원하는 것은 ReadonlyArray<User> 입니다.

중첩된 배열의 평탄화(flatten) 가 필요합니다.

fp-ts/ReadonlyArray 모듈에 있는 함수 flatten: <A>(mma: ReadonlyArray<ReadonlyArray<A>>) => ReadonlyArray<A> 가 바로 우리가 원하는 것입니다:

// followersOfFollowers: ReadonlyArray<User>
const followersOfFollowers = pipe(
  user,
  getFollowers,
  A.map(getFollowers),
  A.flatten
)

좋습니다! 다른 자료형도 살펴봅시다.

예제 (F = Option)

숫자 배열의 첫 번째 요소의 역수를 계산한다고 가정합시다:

import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as A from 'fp-ts/ReadonlyArray'

const inverse = (n: number): O.Option<number> =>
  n === 0 ? O.none : O.some(1 / n)

// inverseHead: O.Option<O.Option<number>>
const inverseHead = pipe([1, 2, 3], A.head, O.map(inverse))

이런, 같은 현상이 발생합니다, inverseHead 의 타입은 Option<Option<number>> 이지만 우리가 원하는 것은 Option<number> 입니다.

이번에도 중첩된 Option 를 평평하게 만들어야 합니다.

fp-ts/Option 모듈에 있는 함수 flatten: <A>(mma: Option<Option<A>>) => Option<A> 가 우리가 원하는 것입니다:

// inverseHead: O.Option<number>
const inverseHead = pipe([1, 2, 3], A.head, O.map(inverse), O.flatten)

모든 곳에 flatten 함수가 있는데... 이것은 우연은 아닙니다. 다음과 같은 함수형 패턴이 존재합니다:

두 type constructor ReadonlyArrayOption (그 외 여러) 은 monad 인스턴스 를 가지고 있습니다.

flatten 은 monad 의 가장 특별한 연산입니다

참고. flatten 의 자주 사용되는 동의어로 join 이 있습니다.

그럼 monad 란 무엇일까요?

보통 다음과 같이 표현합니다...