May 30, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
지난 포스트에서 우리는 F
가 Functor 인스턴스를 허용하는 경우 g
를 lift(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
를 들어 올릴 수 있을까요?
우선 우리는 두 개의 인자를 받아들이고 타입 B
와 C
(튜플을 사용할 수 있다.)를 받아들이고 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 인스턴스에 대한 정상적인 기능이 없습니다.
따라서 이런 푸는 작업을 갖는 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>
따라서 ap
는 unpack
에서 파생될 수 있으며 반대의 경우도 마찬가지로 가능합니다.
참고:
HKT
타입은 제네릭 타입 생성자를 나타내는 fp-ts의 방식입니다. (Lightweight 고급 다형성 논문에서 제안된 기술),HKT<F, X>
를 보면 타입X
에 적용된 타입 생성자F
를 생각할 수 있습니다. (예: F)
또한 타입 A
의 값을 타입 F<A>
의 값으로 들어 올릴 수 있는 기능이 있으면 편리합니다. 이렇게 하면 F<B>
와 F<C>
타입의 인자를 제공하거나 B
와 C
타입의 값을 들어 올림으로써 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 |
아직 해결되지 않은 중요한 경우가 있습니다. 두 프로그램이 모두 이펙트가 있다면 어떨까요?
다시 한번 더 필요한 것이 있습니다. 다음 포스트에서는 함수형 프로그래밍의 가장 중요한 추상화 중 하나인 모나드에 관해 이야기하겠습니다.
요약 : 함수형 프로그래밍은 조합에 관한 것입니다.