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

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

fp-ts 시작하기 (Applicative)

지난 포스트에서 우리는 F가 Functor 인스턴스를 허용하는 경우 glift(g) 함수로 lift(g): (fb: F<B>) => F<C>와 같이 들어 올림으로써 순수한 프로그램 g: (b: B) => C로 이펙트 있는 프로그램 f: (a: A) => F<B>를 조합할 수 있음을 보았습니다.

프로그램 f 프로그램 g 조합
순수한 순수한 g ∘ f
이펙트 있는 순수한 (단항) lift(g) ∘ f

그러나 g는 단항이어야 합니다. 즉, 하나의 인자만 입력으로 받아야 합니다. g가 두 개의 인자를 받아야 한다면 어떻게 하면 될까요? 그래도 Functor 인스턴스만 사용해서 g를 들어 올릴 수 있을까요?

커링

우선 우리는 두 개의 인자를 받아들이고 타입 BC(튜플을 사용할 수 있다.)를 받아들이고 D 타입 값을 반환하는 함수를 모델링해야 합니다.

g: (args: [B, C]) => D;

우리는 커링이라는 기술을 사용하여 g를 다시 작성할 수 있습니다.

커링은 여러 인자를 사용하는 함수의 평가를 각각 단일 인자가 있는 일련의 함수를 평가하는 것으로 변환하는 기술입니다. 예를 들어, 두 개의 인수 (B에서 하나, C에서 하나)를 받고 커링을 통해 D에서 출력을 생성하는 함수는 C에서 단일 인자를 가져와 B에서 C로 출력 함수를 생성하는 함수로 변환됩니다.

g를 아래와 같이 다시 작성할 수 있습니다.

g: (b: B) => (c: C) => D;

우리가 원하는 것은 들어 올리는 작업입니다. 이전 lift와 구별하기 위해 liftA2라고 부르며 아래 시그니처를 갖는 함수를 반환합니다.

liftA2(g): (fb: F<B>) => (fc: F<C>) => F<D>

g는 이제 단항이므로 Functor 인스턴스와 이전의 lift를 사용할 수 있습니다.

lift(g): (fb: F<B>) => F<(c: C) => D>

하지만 이제 막혔습니다. F<(c: C) => D> 값을 함수 (fc: F<C>) => F<D>풀 수 있는 Functor 인스턴스에 대한 정상적인 기능이 없습니다.

Apply

따라서 이런 푸는 작업을 갖는 ap라 불리는 새로운 추상화 Apply를 소개하겠습니다.

interface Apply<F> extends Functor<F> {
  ap: <C, D>(fcd: HKT<F, (c: C) => D>, fc: HKT<F, C>) => HKT<F, D>;
}

ap 함수는 기본적으로 인자를 재배열하여 묶인 것을 풉니다.

unpack: <C, D>(fcd: HKT<F, (c: C) => D>) => ((fc: HKT<F, C>) => HKT<F, D>)
ap:     <C, D>(fcd: HKT<F, (c: C) => D>, fc: HKT<F, C>) => HKT<F, D>

따라서 apunpack에서 파생될 수 있으며 반대의 경우도 마찬가지로 가능합니다.

참고: HKT 타입은 제네릭 타입 생성자를 나타내는 fp-ts의 방식입니다. (Lightweight 고급 다형성 논문에서 제안된 기술), HKT<F, X>를 보면 타입 X에 적용된 타입 생성자 F를 생각할 수 있습니다. (예: F)

Applicative

또한 타입 A의 값을 타입 F<A>의 값으로 들어 올릴 수 있는 기능이 있으면 편리합니다. 이렇게 하면 F<B>F<C> 타입의 인자를 제공하거나 BC 타입의 값을 들어 올림으로써 liftA2(g) 함수를 호출 할 수 있습니다.

이제 Apply를 기반으로 구현되고 이러한 기능(of라고 불리는)을 갖는 Applicative 추상화를 소개하겠습니다.

interface Applicative<F> extends Apply<F> {
  of: <A>(a: A) => HKT<F, A>;
}

몇 가지 일반적인 데이터 타입에 대한 Applicative 인스턴스를 살펴보겠습니다.

예시 (F = Array)

import { flatten } from 'fp-ts/lib/Array';

const applicativeArray = {
  map: <A, B>(fa: Array<A>, f: (a: A) => B): Array<B> => fa.map(f),
  of: <A>(a: A): Array<A> => [a],
  ap: <A, B>(fab: Array<(a: A) => B>, fa: Array<A>): Array<B> =>
    flatten(fab.map(f => fa.map(f))),
};

예시 (F = Option)

import { Option, some, none, isNone } from 'fp-ts/lib/Option';

const applicativeOption = {
  map: <A, B>(fa: Option<A>, f: (a: A) => B): Option<B> =>
    isNone(fa) ? none : some(f(fa.value)),
  of: <A>(a: A): Option<A> => some(a),
  ap: <A, B>(fab: Option<(a: A) => B>, fa: Option<A>): Option<B> =>
    isNone(fab) ? none : applicativeOption.map(fa, fab.value),
};

예시 (F = Task)

import { Task } from 'fp-ts/lib/Task';

const applicativeTask = {
  map: <A, B>(fa: Task<A>, f: (a: A) => B): Task<B> => () => fa().then(f),
  of: <A>(a: A): Task<A> => () => Promise.resolve(a),
  ap: <A, B>(fab: Task<(a: A) => B>, fa: Task<A>): Task<B> => () =>
    Promise.all([fab(), fa()]).then(([f, a]) => f(a)),
};

들어 올리기

그렇다면 F를 위한 Apply 인스턴스가 주어지면 이제 liftA2를 작성할 수 있을까요?

import { HKT } from 'fp-ts/lib/HKT';
import { Apply } from 'fp-ts/lib/Apply';

type Curried2<B, C, D> = (b: B) => (c: C) => D;

function liftA2<F>(
  F: Apply<F>
): <B, C, D>(
  g: Curried2<B, C, D>
) => Curried2<HKT<F, B>, HKT<F, C>, HKT<F, D>> {
  return g => fb => fc => F.ap(F.map(fb, g), fc);
}

좋습니다! 그러나 세 개의 인자가 있는 함수는 어떨까요? 또 다른 추상화가 필요할까요?

좋은 소식은 대답이 “아니요”라는 것입니다. Apply로 충분합니다.

type Curried3<B, C, D, E> = (b: B) => (c: C) => (d: D) => E;

function liftA3<F>(
  F: Apply<F>
): <B, C, D, E>(
  g: Curried3<B, C, D, E>
) => Curried3<HKT<F, B>, HKT<F, C>, HKT<F, D>, HKT<F, E>> {
  return g => fb => fc => fd => F.ap(F.ap(F.map(fb, g), fc), fd);
}

실제로 Apply 인스턴스가 주어지면 각각의 n에 대해 liftAn 함수를 작성할 수 있습니다.

참고: liftA1은 그냥 Functor의 기능인 lift입니다.

이제 “조합표”를 업데이트 할 수 있습니다.

프로그램 f 프로그램 g 조합
순수한 순수한 g ∘ f
이펙트 있는 순수한, n liftAn(g) ∘ f

일반적인 문제가 해결되었나요?

아직 해결되지 않은 중요한 경우가 있습니다. 두 프로그램이 모두 이펙트가 있다면 어떨까요?

다시 한번 더 필요한 것이 있습니다. 다음 포스트에서는 함수형 프로그래밍의 가장 중요한 추상화 중 하나인 모나드에 관해 이야기하겠습니다.

요약 : 함수형 프로그래밍은 조합에 관한 것입니다.


Written by@Minsu Kim
Software Engineer at KakaoPay Corp.