끝판왕 Monad!

  • 모나드에 대해선 읽어보는 블로그마다 설명이 조금씩 달랐습니다. 가장 확실한건 아마 수학적 표현일텐데 그걸 이해하느라 끙끙대고 싶지 않아서, 이 포스트의 내용을 제가 이해한대로 게워내보려 합니다.

  • 우선 인자로 받은 어떤 x 의 sine 값을 출력하는 함수 x와, 그것의 세제곱을 출력하는 함수 y가 있다고 해봅시다.
    const sine = (x) => {
      return Math.sin(x);
    }
    const cube = (x) => {
      return x*x*x;
    }
    
  • 두 함수의 composition은 다음처럼 만들 수 있습니다.
    const sineCube = cube(sine(x));
    
  • 두 함수를 인자로 받아 composite 함수를 만들어주는 compose 라는 함수를 살펴봅시다. ```javascript const compose = (f, g) => { return (x) => { return f(g(x)); } }

const sineOfCube = compose(cube, sine); let y = sineOfCube(x); //(sin(x))^3


- 어떤 요구 사항이 있어 두 함수에서 각각 log를 남겨보고 싶다고 가정합시다. 하지만 이 두 함수는 위의 단순한 compose 함수에 의해 sineOfCube를 생성할 수 없습니다. 당연한 것이, cube와 sine의 연산되어야 하는 값은 하나인데, 숫자와 로그가 합쳐진 배열이 들어갔기 때문에 제 기능을 할 수 없습니다. 
```javascript
const sine = (x) => {
    return [Math.sin(x), 'Sine function was called'];
};

const cube = (x) => {
    return [x*x*x, 'Cube function was called'];
};

compose(cube, sine)(3) // [NaN, "Cube function was called"] !!!!
  • 이 상황을 해결해주는 것이 바로 Monad 입니다. 제가 참고한 링크에서는 모나드를 디자인 패턴이라고 불렀습니다. 디자인 패턴과 디자인 패턴을 구현한 구현체 모두 실제 상황에서는 interchangeable 하게 모나드 라고 부르는 것 같습니다. 자, 그럼 어떻게 이 문제를 해결했을까요?
const composeDebuggable = (f, g) => {
  return (x) => {
    let gx = g(x),      // e.g. cube(3) -> [0,141, 'cube was called.']
        y  = gx[0],     //                 0.141
        s  = gx[1],     //                 'sine was called.'
        fy = f(y),      //     sine(27) -> [0.028, 'sine was called.']
        z  = fy[0],     //                 0.028
        t  = fy[1];     //                 'cube was called.'
    return [z, s + t];
  };
};

composeDebuggable(cube, sine)(3) 
// -> [0.028, 'sine was called.cube was called.']
  • 위와 같이 각 함수의 리턴값을 각각 처리해서 다시 조합해주는 함수 composeDebuggable이 있다면 계산 따로, 로그 따로 훌륭하게 처리가 가능합니다. 여기서 한발 더 나아가 기존의 함수들을 number -> (number, string) 패턴이 아닌 대칭적인 (number, string) -> (number, string) 패턴으로 바꿔주는 함수 bind를 생각해봅시다.
const bind = (f) => {
  return (tuple) => {
    var x  = tuple[0],
        s  = tuple[1],
        fx = f(x),
        y  = fx[0],
        t  = fx[1];
    return [y, s + t];
  };
};
  • bind 함수는 두개의 인자를 받아 각각 연산한 결과 (숫자는 계산, 스트링은 concatenate)를 리턴하도록 원래 함수를 변형시켜주는 역할을 합니다. bind를 이용하면 원래 정의되었던 compose 함수를 다음처럼 사용할 수 있습니다.
let c = compose(bind(sine), bind(cube));
c([3, '']); // -> [0.028, 'sine was called.cube was called.']
  • 또 한발자국 더 나아가, 이제는 애초에 [3, ‘’] 이라는 배열 대신, 숫자 하나만 넣고 싶습니다. 로깅은 결국 사용자가 넣어줘야하는 파라미터가 아니기 때문에 입력으로 받는 것이 이상합니다. 그 역할은 unit이 한다고 합시다.
    unit :: number -> (number, string)
    const unit = (x) => { return [x, '']};
  • unit의 역할은 값을 받아서 함수가 처리할 수 있는 형태로 변환시켜 주는 것입니다. 이제 우리의 함수는 이렇게 보입니다.
const c = compose(bind(cube), bind(sine));
c(unit(3)); /// -> [0.028, 'sine was called.cube was called.']
  • 마지막으로, 단일함수 sine만 debuggable하게 바꾸고 싶다면 어떻게 해야할까요? 정답은 바로 unit을 적절히 활용하는 것에 있습니다.
const round = (x) => return Math.round(x);
const roundDebug = (x) => return unit(round(x));

const c = compose(bind(roundDebug), bind(sine));
c(unit(100)); //some value I don't know!!
  • 위에서 언급된 unit과 compose가 바로 모나드를 정의합니다. unit과 compose는 무엇을 한 걸까요? 바로 연산을 합하는 일을 했습니다.

  • 데이터 추상화에서 나아가, 연산들마저 어떤 구조로 추상화하여 그것들의 연산 가능성이 보장된다면 코드의 의미가 더 간결해지고 재사용성이 커지게 됩니다. 제가 찾은 훌륭한 글에 따르면,

어떤 타입 M에 대해 pure(bind)와 compose가 존재할 때, M은 모나드 이다.

  • 라고 합니다. 이 연산의 합성이 어떤식으로 유용하게 쓰이는지는 아직 몸으로 경험해본 적이 없지만, 그래도 두루뭉실하게 이해가 되는 느낌입니다. 어떤 pure와 compose가 존재하는 모나드끼리는 마구마구 합성해도 (chaining!!) 우리는 원하는 결과를 얻을 수 있는거죠. 중간에 귀찮은 형변환, 타입 변환, null 체크 등등을 건너뛰고 F o G o K o ….를 할 수 있다고 생각하면 될까요?

  • 맥스웰의 방정식을 알고있다고 전기 기술자가 되는 것이 아니듯, 이론을 실제로 이해하고 체화하려면 많은 경험이 필요할 것입니다. 언젠가는 Functor, Applicative, 그리고 Monad의 참맛을 느껴보고 싶습니다.