FP Workshop - 3. Monad


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);