fp-ts로 Typescript 함수형 프로그래밍 시작하기 11 (Reader)

본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.

fp-ts 시작하기 (Reader)

Reader 모나드의 목적은 필요한 곳에서 인자를 얻기 위해 여러 함수를 통해 인자가 전달되는 것을 피하는 것입니다.

여기에 제시된 아이디어 중 하나는 의존성 주입을 위해 Reader 모나드를 사용하는 것입니다.

가장 먼저 알아야 할 것은 Reader<R, A> 타입이 함수 (r : R) => A를 나타낸다는 것입니다.

interface Reader<R, A> {
  (r: R): A
}

여기서 R은 계산에 필요한 “환경”을 나타내며(여기에서 “읽을” 수 있음), A는 계산의 결과입니다.

예시

아래와 같은 코드가 있다고 가정해 보겠습니다.

const f = (b: boolean): string => (b ? 'true' : 'false')

const g = (n: number): string => f(n > 2)

const h = (s: string): string => g(s.length + 1)

console.log(h('foo')) // 'true'

만약 f 함수에 국제화가 필요하다면 우리는 다른 매개 변수를 추가할 수 있습니다.

interface Dependencies {
  i18n: {
    true: string
    false: string
  }
}

const f = (b: boolean, deps: Dependencies): string =>
  (b ? deps.i18n.true : deps.i18n.false)

하지만 g 함수는 더 이상 컴파일되지 않는다는 문제가 발생했습니다.

const g = (n: number): string => f(n > 2)
// error: An argument for 'deps' was not provided

우리는 g 함수에도 매개변수를 추가해야 합니다.

const g = (n: number, deps: Dependencies): string =>
  f(n > 2, deps) // ok

아직 끝나지 않았습니다. 이제는 h 함수가 컴파일되지 않습니다. 우리는 h 함수에도 매개변수를 추가해야 합니다.

const h = (s: string, deps: Dependencies): string =>
  g(s.length + 1, deps)

마침내 우리는 Dependencies 인터페이스의 실제 인스턴스를 제공해 h 함수를 실행할 수 있습니다.

const instance: Dependencies = {
  i18n: {
    true: 'vero',
    false: 'falso'
  }
}

console.log(h('foo', instance)) // 'vero'

예시에서 볼 수 있듯이 hg 함수는 f 함수의 의존성을 사용하지 않아도 의존성에 대한 지식이 있어야 합니다.

이 부분을 개선 할 수 있을까요? 네 가능합니다. Dependencies를 매개변수 목록에서 반환 타입으로 이동할 수 있습니다.

Reader

우선 deps 매개 변수는 그대로 두고 함수를 다시 작성하겠습니다.

const f = (b: boolean): ((deps: Dependencies) => string) => deps =>
  (b ? deps.i18n.true : deps.i18n.false)

const g = (n: number): ((deps: Dependencies) => string) => f(n > 2)

const h = (s: string): ((deps: Dependencies) => string) => g(s.length + 1)

참고: (deps: Dependencies) => stringReader<Dependencies, string>일 뿐입니다.

const f = (b: boolean): Reader<Dependencies, string> => (deps) =>
  b ? deps.i18n.true : deps.i18n.false;

const g = (n: number): Reader<Dependencies, string> => f(n > 2);

const h = (s: string): Reader<Dependencies, string> => g(s.length + 1);

console.log(h('foo')(instance)) // 'vero'

ask

g 함수에 하한 (예시에서는 2)도 주입하려면 어떻게 할 수 있을까요? 먼저 Dependencies에 새 필드를 추가하겠습니다.

interface Dependencies {
  i18n: {
    true: string;
    false: string;
  };
  lowerBound: number;
};

const instance: Dependencies = {
  i18n: {
    true: 'vero',
    false: 'falso',
  },
  lowerBound: 2,
};

이제 우리는 ask를 사용하여 환경에서 lowerBound를 읽을 수 있습니다.

원문에서는 pipeable/pipe를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며 function/pipe를 사용하면 됩니다.

import type { Reader } from 'fp-ts/lib/Reader';
import { pipe } from 'fp-ts/lib/function';
import { ask, chain } from 'fp-ts/lib/Reader';

const g = (n: number): Reader<Dependencies2, string> =>
  pipe(
    ask<Dependencies2>(),
    chain((deps) => f(n > deps.lowerBound)),
  );

console.log(h('foo')(instance)) // 'vero'
console.log(h('foo')({ ...instance, lowerBound: 4 })) // 'falso'

추신: Readermap은 일반적인 함수 조합 입니다.

import { flow, pipe } from 'fp-ts/lib/function'
import { map } from 'fp-ts/lib/Reader'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2
const gt2 = (n: number): boolean => n > 2

const compositionWithFlow = flow(len, double, gt2)
// 아래와 같다.
const compositionWithPipe = pipe(len, map(double), map(gt2))

Written by@Minsu Kim
Software Engineer at KakaoPay Corp.