FP Workshop - 3. Monad
Table of contents
Chapter 3: Monad
1. Monad(모나드)와 Promise
모나드란 함수합성을 안전하게 해주는 장치.
함수 f와 g를 합성한다고 할때, 수학적인 기호로는 다음과 같이 표시한다.
f * g
이론적으로는 f(g(x)) 를 합성한 결과는 언제 어디서 실행하든지 항상 f(g(x))가 되어야 하지만, 현실 에서는 여러가지 외부적인 요인(메모리누수, API 서버 고장 등)으로 인해 같지 않을 수 있다.
f(g(x)) == f(g(x)) // 이론
f(g(x)) != f(g(x)) // 현실
모나드는 함수합성을 안전하게 하기위한 목적을 가지고 만든 객체(Object)다. 이 객체가 map을 메소드로 가지고 있으면 functor(함수자), map과 flatMap을 메소드로 가지고 있으면 monad라고 정의하였지만 이것은 그냥 정의일 뿐. 모나드가 우리에게 어떤 가치가 있는지 이해하는것이 중요하다.
1) Array
Array를 일종의 모나드로 생각해볼 수 있다.
f(g(x)) == f(g(x)) // 일반적인 경우
f(g(x)) == 실행안함 // 비어있는 배열인 경우
Array 모나드는 다음과 같은 목적으로 만들어진 모나드
- Array가 비어있다면 함수합성을 하지 않는다.
- 여러 요소의 함수합성을 가능하게 해준다.
js
const g = (a) => a + 1;
const f = (a) => a * a;
[1]
.map(g)
.map(f)
.forEach((a) => console.log(a)); // 4
[1, 2, 3]
.map(g)
.map(f)
.forEach((a) => console.log(a)); // 4, 9, 16
[]
.map(g)
.map(f)
.forEach((a) => console.log(a)); // 아무일도 안함
2) Promise
Promise 도 Monad의 관점에서 바라본다면 다음과 같다
f(g(x)) == f(g(x)) // 일반적인 경우
f(g(x)) == g(x) // 비동기중 오류가 발생했을때 합성을 중지
js
const g = JSON.parse;
const f = ({ k }) => k;
Promise.resolve('{"k": 1}')
.then(g)
.then(f)
.then(log)
.catch(log);
2. Monad의 활용
예시로 활용할 products 리스트
js
const products = [
{ id: "candy", name: "Candy", price: 10, onSale: true },
{ id: "ice-cream", name: "Ice cream", price: 20, onSale: true },
{ id: "cake", name: "Cake", price: 30, onSale: false },
{ id: "donuts", name: "Donuts", price: 15, onSale: true },
{ id: "chocolate", name: "Chocolate", price: 12, onSale: false },
{ id: "flower", name: "Flower", price: 40, onSale: false },
{ id: "sofa", name: "Sofa", price: 120, onSale: true },
{ id: "bed", name: "Bed", price: 400, onSale: true },
];
1) Maybe
함수 합성중 에러가 발생할 시 우회하기 위한 모나드
js
// 정의
class Maybe {
constructor(value) {
this.$value = value;
}
static of(value) {
return new Maybe(value);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
toString() {
return this.isNothing ? "Nothing" : `Just(${this.$value})`;
}
chain(fn) {
return fn(this.$value);
}
}
// 활용
Maybe.of(products)
.map(find((p) => p.id === "candy"))
.map(prop("price"))
.chain(p => p)
2) Either
값에 따라 어떤 함수를 합성할지 선택하는 모나드
js
class Either {
constructor(value) {
this.$value = value;
}
static right(value) {
return new Right(value);
}
static left(value) {
return new Left(value);
}
}
class Right extends Either {
get isRight() {
return true;
}
get isLeft() {
return false;
}
map(fn) {
return new Right(fn(this.$value));
}
chain(fn) {
return fn(this.$value);
}
}
class Left extends Either {
get isRight() {
return false;
}
get isLeft() {
return true;
}
map(fn) {
return this;
}
chain(fn) {
return this;
}
}
const either = curry((l, r, e) => {
return e.isLeft ? l(e.$value) : r(e.$value);
});
pipe(
find((p) => p.id === "candy"),
(product) => (!product ? Either.left("Sorry!") : Either.right(product)),
either(
(l) => console.log("Product not found", l),
(r) => console.log("Product : ", r),
),
)(products);