OVER THE CODE

JS개발자는 아직도 모나드를 모르겠어요

May 16, 2020

그렇다. 이 글은 수많은 모나드 튜토리얼 중 하나이다. 그러나 부리토와 우주복을 입은 우주비행사 같은 비유를 꺼내려는 게 아니다. 하스켈의 지식들을 끌어와 모나드가 무엇인지 설명하지도 않을 것이다. 많은 모나드 튜토리얼들이 Maybe , Eiter모나드를 구현하며 개념을 설명하려고 한다. 그러나 이건 우리가 일상적으로 마주하는 코드에서 전혀 보이지 않는다. 어떤 글이 Left identity, Right identity, Associativity와 같은 모나드 법칙을 꺼내는 순간 우리는 조용히 탭을 닫고 중얼거린다. “음.. 이해했어”

결국 많은 글을 읽고도 누가 모나드에 대해 물으면 ”Maybe라는게 있는데…”로 시작해 lift, unit, of, flatten, join, map, chain, bind같은 용어들을 웅얼거릴 수 밖에 없다.

이 글은 타입클래스에 대해 관심을 갖게 되어 몇번이나 인터넷을 뒤적거리다가 결국 아리송 한 채 머리를 긁적이는 자바스크립트 개발자들을 위한 글이다. 왜? 언제? 모나드가 필요한지, 어떤 코드에서 모나드를 발견할 수 있는지, 그리고 우리의 이해를 방해하는 요소들이 무엇인지 알아볼 것이다. 이 글의 핵심은 너무 엄격해지지 말 것이다.

우린 이미 펑터에 관한 많은 글들을 읽었다. 펑터란 map 인터페이스를 가지고 있는 타입이라고 한다. 어떤 글에선 Array를 가지고 설명한다. 또 어떤 글에선 다음과 같이 구현한다.

class Functor {
  constructor(value) {
    this.value = value
  }
  map(fn) {
    return new Functor(fn(this.value))
  }
}

일반 함수로 구현할 수 도 있다.

const Functor = (value) => ({
  value,
  map: (fn) => Functor(fn(v))
})

하스켈을 공부한 사람은 다음과 같이 구현한다.

const Functor = (value) => ({
  fmap: (f) => Functor.of(f(value)), // 하스켈에선 map이 아니라 fmap이다.
  show: () => `Functor(${ value })`, // toString과 동일한 어떤 것.
  equals: (Other) => value === Other.value, // Eq도 추가해주자.
  ...
})
Functor.of = Functor // Unit 함수

그렇다면 이 코드는 어떤가?

const cat = (is, awesome) => ({ ...awesome, value: is(awesome.value) })

생성자는 없고 함수 하나만 달랑 있다. 심지어 네이밍도 제멋대로지만 우리가 원하는 동작을 한다. 그렇다면 우린 여기서 무얼 보고 펑터라 불러야 하는가? value 속성을 가진 타입의 오브젝트와 cat함수는 펑터를 형성한다고 말할 수 있다.

즉, map이란 이름은 펑터의 본질이 아니다. map이 어떤 오브젝트의 메소드로 존재해야하는 것도 아니다. 생성자가 있어야하는 것도 아니다. 타입, 카테고리 이론과 형식에 얽매이다 보면 본질을 파악하기 힘들다. 그래서 결국 무엇을 하는 것인데? 라는 의문을 품자. 펑터란 어떤 값을 들고 있는 구조와 구조를 유지한채 그 값에다 함수를 적용할 수 있는 인터페이스의 조합이다. 어떤 코드를 보고 “펑터처럼 생겼네” 라고 할 수 있으면 충분하다. 펑터 법칙을 증명하는 일은 부지런한 사람들에게 맡기자.

모나드로 가보자. 모나드는 펑터인 동시에 join/flattenmap을 가지고 만든 chain/bind/flatMap 메소드가 있으며 클레이슬리 컴포지션이란, 모나드 법칙이란.. 등등

이런 설명들은 도통 도움이 되지 않는다. 다음과 같은 예시는 우리를 더욱 혼란스럽게 만든다.

class Monad {
  static of(value) {
    return new Monad(value);
  }
  constructor(value) {
    this.value = value
  }
  map(fn) {
    return Monad.of(fn(this.value))
  }
  join() {
    return this.value
  }
  chain(fn) {
    return fn(this.join())
  }
}

// examples
Monad.of(3).map(x => x+3).chain(x => Monad.of(x*2)).join() // 12
...
  • join 메소드는 왜 필요한거지? 그냥 .value로 접근하면 안돼?
  • chain 은 그냥 return fn(this.value) 로 해도 되지 않나?
  • of는 왜 있는거야? new 는 쓰지말란건가?
  • chainx => x 를 넣으면 join 아닌가?
  • 그래서 결국 이걸로 할 수 있는게 뭔데?

이런 의문들이 떠오른다면 거의 다 온 것이다. 다음 코드를 보자

// jQuery
$("#foo").val()

// lodash
_.chain(users)
  .sortBy("age")
  .map(({name, age}) => `${name} is ${age}`)
  .head()
  .value()

// moment
moment()
  .add(7, 'days')
  .subtract(30, 'seconds')
  .format()

모두 어떤 컨택스트로 감싸진 값을 빼내는 행위를 하고있다. 일종의 join인 셈이다. (물론 어느 것도 모나드가 될 수 없다. 필요한 메소드도 없고 법칙을 만족하지도 않는다.)

그렇다면 chain은 어디서 볼 수 있는가? 거의 볼 수 없다. 그렇기 때문에 우리는 모나드를 이해하기 어려웠던 것이다. chain은 point-free style이 강요되고 절차적인 코드를 작성하기 힘든 환경에서 등장한다. 자바스크립트는 point-free style을 지원하지만 그보다는 const value = someMonad.join() 처럼 중간 값을 저장하는 방식으로 코드를 작성한다. 중간 값을 저장할 수 없고 순수하게 함수를 합성하는 방식으로 코드를 작성해야 하는 환경에서 chain이 빠질 수 없는 요소가 되는 것은 너무 당연하다.

하스켈의 예를 잠깐 보자.

// main = readFile "index.html" >>= putStrLn
const main = () => readFileAsync("index.html").then(console.log)

// main = do
//     x <- readFile "index.html"
//     putStrLn x
const main = async () => {
  const x = await readFileAsync("index.html")
  console.log(x)
}

주석으로 처리된 하스켈 코드는 IO 모나드를 사용해 입출력을 처리한다. 하스켈의 do notation은 절차적인 형태로 코드를 작성할 수 있게 하는데, 이를 다시 보면 자바스크립트 코드와 형태가 매우 유사하다는 것을 알 수 있다. (그러나 Promise는 모나드가 아니다. 법칙을 만족하지 않기 때문이다.)

마저 궁금증을 풀어보자. class Monad 는 어떤 쓸모가 있는가? 아무 쓸모도 없다. 단지 누군가가 모나드를 설명하기 위해 억지로 만들어낸 코드일 뿐이다. 굳이 이름을 붙이자면 Id모나드 정도로 부를 수 있다. MaybeEiter는 어디서 발견할 수 있는가? 하스켈로 가면 된다. 어떤 글에선 Maybe 모나드로 null체크를 추상화하지만 자바스크립트는 옵셔널 체이닝이라는 훨씬 유연하고 강력한 기능을 제공한다. of는 왜 있어야 하는가? 없어도 된다. chain 내부에서 join을 호출할 필요 없이 바로 값에 접근해도 된다. 자바스크립트는 매우 유연한 언어기 때문에 순수 함수형 프로그래밍 언어에서 보이는 형태를 자바스크립트로 작성하려는 과정에서 많은 억지와 불필요한 가정들이 생겨나는 것이다.

어린이들이 사칙연산을 배울 때 이항연산자, 결합법칙, 교환법칙, 항등원을 함께 배우지는 않는다. 각 연산이 무엇을 하는지 배우기 위해서 여러 법칙들을 알아야만 했다면 아무도 수학을 배울 수 없었을 것이다. 함수형 프로그래밍을 배우는 사람들이 카테고리 이론과 타입 클래스를 대하는 자세도 이와 같아야 한다. 코드의 형태와 무관하게 그들의 인터페이스가 어떤 동작을 하기 위해 정의 되었는지 이해하는 것이 우선이고, 엄격한 정의를 찾아보는 것이 그 다음이 되어야 한다. 그래야만 코드 속에 숨겨진 모나드, 펑터의 흔적을 찾으며 “아하” 라고 외칠 수 있다. 그리고 그 “아하”란 우리 머릿속에 깊은 추상화가 생겨나는 소리이자 이 모든 것에 시간과 노력을 들여 공부하는 의미이다.

그래서 결국 모나드란 무엇인가? 모나드는 펑터인 동시에 내부의 값에 직접 함수를 적용할 수 있는 구조와 인터페이스이다. 엄격하게 정의하기 위해선 세가지 모나드 법칙을 만족한다는 것을 증명해야하지만 이 글의 핵심을 잊으면 안된다. 너무 엄격해지지 말자.


© Karl Saehun Chung