모나드 문턱에서

Posted on June 24, 2020
왜 하는지 먼저 알려주면 안될까?

※ 2022-04-04 수정: 카테고리 이론과 관련된 글이 일부 있었는데, 부정확한 수학 글은 모두 걷어냈습니다.
※ 모나드 관련글을 새로 올렸습니다. 여기 보다는 모나드, 같음 글을 봐주세요.

  1. 모나드가 뭘까?
  2. a -> m b 타입을 합성하면 모나드인가?
  3. a -> m b 타입 함수가 특별할까?
  4. 자신을 품고 있는 함수(상위 함수)의 매개 변수 값은 접근 할 수 있다 Closure
  5. 컨텍스트 Context
  6. 비순수 작업은 모나드가 아니라 펑크터가 담당한다
  7. 상태를 넘기는 영리한 방법이야?
  8. 모나드가 왜 수많은 난제들을 해결할까?
  9. 엮어 놓은 액션(함수) 뭉치 실행(run)

모나드가 뭘까?

펑크터 m 구조에 함수 두 개 return: a -> m a,join: m (m a) -> m a가 있어, m (m (m ...))합성이 가능한 구조를 모나드라 합니다. 말로 풀면, 두 함수의 도움으로 같은 펑크터를 몇 번이고 계속 적용해도 한 번 적용한 것과 같은 타입으로 볼 수 있는 구조를 모나드라 합니다. m을 두 번 이상 적용한 것을 한번 적용한 것으로 만들 수 있습니다.

-- m (m a) -> m a 가 있어 아래처럼 할 수 있습니다.
m (m (m (m ...))) -> m

a -> m b 타입을 합성하면 모나드인가?

모나드 구조에 있는 함수(카테고리 이론에선 Natural Transformation)를 이용하면 a -> m b 타입 합성이 가능한 것이지, 이 합성 자체가 모나드인 건 아닙니다. 모나드의 합성을 얘기할 때는 m a, m (m a), m (m (m a)), m (m (m (m a))),... 이렇게 펑크터m의 합성을 말하는 것입니다. m의 생성자도 비슷한 타입 a -> m a이어서 혼란을 주기도 합니다. 하스켈에서 모나드를 만날 때는 대부분 a -> m b 합성에서 만나니 둘을 혼동할만 합니다.

a -> m b 타입 함수가 특별할까?

m a -> m b 타입으로 함수를 만들면 복잡한 개념 없이 품고 품게composition 할 수 있지 않을까 생각이 듭니다. 하지만, m 컨텍스트에서 m 컨텍스트로 가면 품는 동안 반드시 m 컨텍스트가 발현된다는 보장이 없습니다. m을 다루는 함수면 m안에 있는 건 뭔지 신경쓰지 않을 수도 있습니다. fmap이 동작하거나, 패턴 매칭이 나와야지만 effect1가 드러납니다. m을 열어 보는 순간이 있어야 합니다. m aa -> m b 함수를 받으면, m a 에서 m 을 벗겨내는 작업을 해야만 a -> m b 함수를 적용할 수 있습니다. 다시 말해, 컨텍스트가 반드시 발현됩니다. 수학이 아닌, 순수하게 프로그래밍 입장에서 바라 봤습니다. 참고 - 값을 감싸는 이유

2021.5.21 추가
또 한가지 중요한 이유가 있습니다. 바로 클로저를 활용하기 위해서입니다. 아래 클로저부분 참고, Parsec 글 참고
하스켈에 쓰는 수많은 모나드들이 람다 변수를 메모리처럼 쓰는 패턴을 이용합니다. 모나드가 하스켈 온 사방에 나올 수 밖에 없는 이유입니다. 아래 Closure 참고 a -> m b가 특별한 이유를 이렇게 설명하는 자료는 아직 찾지 못했습니다. 검증필요

아무리 그래도 모나드는 어렵습니다.

※ 2023.1.7 가방에 적혀있던 글자를 CLOSURE에서 EFFECT로 바꿨습니다.

자신을 품고 있는 함수(상위 함수)의 매개 변수 값은 접근 할 수 있다 Closure

람다 대수를 보면

𝜆x.(𝜆y.xy)

𝜆y를 매개 변수로 갖고 있는 함수에서 𝜆x에 접근 할 수 있습니다. 𝜆x.( ... ) 인 함수는 당연히 𝜆x를 … 에서 쓸 수 있어야 하니 당연한 결과입니다. 하스켈도 \x -> (\y -> x + y) 가 가능합니다. 매개 변수 이외에는 외부와 소통할 수 없는 함수형 프로그래밍에서의 함수에게는, 이 특징이 매우 유용하게 쓰입니다. 품고 품게(연이은 composition) 해서 엮은 함수들의 가장 바깥쪽에서 넣어 주는 인자값은, 그 함수 뭉치에서 전역값 같은 역할을 합니다.

p1 >>= \r1 ->
p2 >>= \r2 ->
p3 >>= \r3 ->
p4 >>= \r4 ->
p5 >>= \r5 ->
return (somef r1 r2 r3 r4 r5)

-- do로 표기하면
do 
   r1 <- p1
   r2 <- p2
   r3 <- p3
   r4 <- p4
   r5 <- p5
   return (somef r1 r2 r3 r4 r5)

람다 변수를 함수들의 결과를 저장해 두는 곳으로 씁니다. 바인드를 파워풀하게 만드는 또 하나의 요소입니다.

참고 - 상태 개념 포스트

컨텍스트 Context

문장과 문장 사이, 행간으로 번역하기도 하고, 맥락이라고 번역하기도 하는데, 무언가 명시적으로 언급하진 않았지만 현재 작업을 할 때 영향을 미치는 환경, 상황 같은 걸 말합니다. 코딩에서는 어떤 모양으로 컨텍스트가 나타날까요? 같은 성격의 작업을 할 때마다 항상 반복해서 실행되는 코드가 바로 컨텍스트입니다. 바인드 안에서 fmap을 불러 항상 effect 처리를 하는데, 바인드로 체이닝되어 연결될 때마다 effect 처리가 반복되는 걸 컨텍스트라 부릅니다.

“컨텍스트를 이용해서 Side effect가 있는 비순수 함수와 같은 결과를 순수 함수로 만들어낸다”라고 말해야지, “컨텍스트는 side effect를 의미한다”라고 하면 틀린 말입니다.

참고 - 컨텍스트, Applicative Functor, Traversable

비순수 작업은 모나드가 아니라 펑크터가 담당한다

예를 들어 Maybe의 컨텍스트인 “Nothing일 수도 있어” 라는 컨텍스트를 처리하는 건 펑크터의 fmap이 맡습니다. fmap에서 Nothing이냐, Just냐에 따라 패턴 매칭이 일어납니다. 이렇게 작업한 후 결과를 다음 작업과 연결을 위한 준비 작업을 담당하는게 모나드입니다. 모나드의 bind 구현을 보면 joinfmap을 품은 형태입니다. fmap으로 effect를 발현시키고 나온 결과물을 다음 액션과 연결하기 위해 join을 씌우는 모양입니다.

x >>= f =   join   (fmap f x)

상태를 넘기는 영리한 방법이야?

이건 특정 모나드의 동작일 뿐입니다. “상태를 넘기는 걸 모나드 패턴으로 구현할 수도 있어”가 맞는 표현입니다. 모나드를 특정 문제를 위한 패턴으로만 접근하면 꽤 멀리 돌아서 모나드에 도착할지도 모릅니다.

모나드가 왜 수많은 난제들을 해결할까?

함수가 함수를 품을 때마다 반드시 fmap이 한 번씩 동작합니다. 체이닝할 때, 매 번 뭔가 작업을 반복해서 하고, 결과를 잃어버리지 않고 누적해야 한다면 딱 어울리는 패턴입니다.

Maybe는 컴포지션할 때마다 매 번 “Nothing 인지 검사하고”
Status는 컴포지션할 때마다 매 번 “상태값을 액션에 넣어줘야 하고”
Writer는 컴포지션할 때마다 매 번 “로그 ‘같은’ 값들을 계속 누적하고” IO는 컴포지션할 때마다 매 번 “Status처럼 런타임 상태값realworld을 계속 끌고 다녀야 하고”

(거의 대부분 “순수 함수는 매개 변수로만 소통할 수 있다”라는 제약 때문에 등장한 난제들입니다.)

그리고 또 한가지,
카테고리 이론 쪽에선 의도한게 아닐 것 같은데요, 모나드의 굉장히 중요한 성격이 있습니다. 하스켈은 Lazy한 함수들이 순차적으로 실행된다는 보장이 없습니다. 하지만 함수들이 품고 품은 상태로 엮인 상태에서, 현재 함수를 실행하려면 반드시 이전 단계 함수의 결과값이 꼭 필요한 상태가 되게 하면, 자연스럽게 함수들 사이에 실행 순서가 생깁니다. 모나드 패턴으로 엮을 때 이 효과가 자연스럽게 생깁니다. 수학에서 개념을 가져왔지만, 수학 개념에서 주목하지 않았던 특징들도 나타납니다.

정리하면, 함수를 컴포지션할 때마다 항상 처리해야 하는 작업(effect가 생기는 작업)이 있다거나, 실행 순서가 지켜져야 하는 작업이라면, 모나드가 필요한지 살펴 볼만 합니다.

초창기 하스켈에는 모나드가 없었다고 하는데, 그 때는 IO 같은 작업을 어떻게 해결했을까 궁금하긴 합니다.

(※ stream, continuation 등을 이용했다고 합니다.)

엮어 놓은 액션(함수) 뭉치 실행(run)

monadChain = runReader (do
   ...
   ...
   ) env

(\x...(\y...(\z...))) 이렇게 엮어 놓은 함수 뭉치는 블랙박스로 바라보면 (\x ...) 하나의 함수일 뿐입니다.

do 이하 구문들은 여러 개의 bind로 품고 품게 되어 있는 함수 뭉치입니다. 아직 실행되지 않은, 언젠가 실행될 하나의 함수입니다. 이 뭉치의 가장 바깥쪽 함수에 env를 넣어주면, 그제서야 함수 뭉치의 실행이 “시작”됩니다. 그래서 모나드들이 레코드 필드 접근자의 이름을 run~ 이라고 짓는 경우가 많습니다. 마치 골드버그 장치가 실행되는 것처럼, 전체가 동작을 시작하는 트리거를 표현하니 run~이란 이름이 적당해 보입니다.

미완성…

※ 모나드 관련글을 새로 올렸습니다. 여기 보다는 모나드, 같음 글을 봐주세요.


  1. Effect effect는 아래와 같이 정의하지 않지만, 모나드 관련해선 아래와 같이 해석합니다. m a -> a로 오는 동안 사라지는 정보를 effect라 부릅니다. 만일 사라지는 정보가 없다면(둘이 isomorphic 하다면), m a는 effect가 없다고 말합니다.

    Computation 예
    - partiality
    - nondeterminism
    - side-effect
    - exceptions
    - continuations
    - interactive input
    - interactive output
    [출처 Notions of computation and monads - Eugenio Moggi]

    (사전적으로는 컴퓨터가 계산하는 작업을 computation이라 부릅니다.) T AA의 computation 결과, T를 notion of computation이라 합니다. a -> m b 함수는 m이 모나드라면 monadic computation

    튜링 머신으로 reduce되는 어떤 것이든 computation이라 부릅니다. 1+1이란 식을 그대로 바라보면 computation이라 부르지 않지만, 이 식을 컴퓨터에 넣어 결과를 가져오기 위해, 컴퓨터에 준비한 1+1은 computation이라 부릅니다.

    Computational effect
    computaion으로 인해 생기는 effect

    Effectful function
    effect를 만들어내는 함수, effect가 있는 타입을 다루는 함수

    Effect algebra 두 번의 computation으로 생기는 두 개의 effect를 합칠수 있는 연산 정의가 가능해야먄 모나드로 만들 수 있습니다. effect algebra가 가능한 effect만 모나드로 만들 수 있습니다.↩︎

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com