Monad
Functor와 마찬가지로 타입 A에서 타입 B로 매핑하는 interface인데, 일반 map이 아닌 flatMap을 사용한다는 차이점이 있습니다.
flatMap
아래 두 연산들을 순서대로 합쳐서 구현한 매핑연산입니다.
map
functor context 상에서 특정 monad를 다른 monad로 매핑합니다.
flatten
functor 매핑으로 추가된 가장 외부의 monad context 하나를 제거합니다.
context
Functor, Monad의 mapping 중 수행되는 연산들을 추상화하는(= 독립된 단위라서 사용자로부터 내부 구현을 은닉해도 되는) wrapper입니다.
Array에 대해 map
을 수행하면 iteration 일어나고, Promise에 대해 then
을 수행하면 resolve될 때까지 대기하는 등의 내부연산들이 context로서 수행됩니다.
monad의 매핑연산은 아래와 같은 수식(?)으로 표현할 수 있습니다.
M(a).flatMap(a => M(b))
= M(a).map(a => M(b)).flatten()
= M(M(b)).flatten()
= M(b)
// flatMap에 들어가는 arrow는 functor(a ⇒ b)와는 다르게
// type lifting 함수(a ⇒ M(b))가 들어갑니다.
type lifting 함수
특정 입력타입이 들어오면 context로 wrapping된 출력타입을 반환하는 함수입니다.
Composition
arrow들을 연속으로 합성하거나, 단일 arrow로부터 다수 arrow들을 분리하여 Monad를 계산할 수 있습니다.
f = a => M(b)
g = b => M(c)
// f, g는 type lifting 함수로, arrow로 사용됩니다.
M(a).flatMap(h)
= M(a).flatMap(f).flatMap(g)
= M(b).flatMap(g)
= M(c)
Monad를 JS로 구현하면 다음과 같습니다.
const Monad = (value) => ({
map: (f) => f(value),
flatMap(fn) {
return this.map((a) => Monad.of(fn(a)));
},
});
Monad.of = (x) => Monad(x); // type lifting
Monad(21)
.flatMap((x) => x * 2) // Monad(42)
.flatMap((x) => console.log(x)); // 42
Promise
Promise는 Monad의 대표적인 예시입니다.
- flatMap 대신
then
메서드를 사용하고, - 이전 promise의 resolved value가 unwrap(= flatten)되어 전달되기 전까지 다음
then
의 arrow는 실행되지 않고, - resolved value가 중첩된 Promise여도(=
P(P(P(…)))
) 모두 unwrap 되어 다음then
의 arrow로 구체적인 값만 전달됩니다.
Promise 기반의 Monad 매핑은 아래와 같은 함수로 구현할 수 있습니다.
const composeMonads =
(flatMap) =>
(...arrows) => {
return arrows.reduce((f, g) => (x) => f(x)[flatMap](g));
};
const composePromises = composeMonads('then');
// arrow들은 모두 하나같이 a => Promise(a) 형태로 가지므로,
// flatMap 부분에서는 flatten 연산(Promise(x) => x)이 필요합니다.
const getUserById = id => id === 3
? Promise.resolve({ name: 'Kurt', role: 'Author' });
: undefined;
const hasPermission = ({ role }) => {
return Promise.resolve(role === 'Author');
}
const authUser = composePromises(getUserBuyId, hasPermission);
// x => getUserBuyId(x).then(hasPermission)
authUser(3); // Promise({ name: 'Kurt', role: 'Author' })
TIP
composePromises와 같이 일련의 type lifting 함수들을 합성한 함수를 "Kleisli Composition Function"이라고 부릅니다.