data Maybe T = Just T or Nothing
시작은 함수형 프로그래밍이다.
개인적인 결론은, 협업할 때 사용하면 아주 좋은 프로그래밍 스타일이라고 생각했다. 특정 함수를 사용하면 그 결과값을 예상 할 수 있어야한다. 이 부분이 아주 매력적이었다. 아무리 하드 코딩을 기피하며 변수 네이밍을 기가막히게 하고, 주석으로 추가 설명을 하더라도 다른 사람의 생각을 구현한 코드를 이해하기 어려울 때가 있다. 그럴 때,
이 함수는 이렇게 실행합니다. a 라는 값을 넣으면 항상 b라는 값이 나와요.어떤 값을 넣어도 같은 형식으로 처리되니 걱정하지 말고 쓰세요.
라고 말해준다면 얼마나 좋을까.(물론 신뢰할 수 있는 동료들 사이에서)
아무튼 위와 같이 말하기 위해서는 몇 가지 조건을 충족하여 함수를 작성해야하고, 위와 같은 조건을 충족한 코드를 함수형이라고 한다. 어쨌든 함수형 프로그래밍과의 첫 만남은 이랬다. 아주 좋았다. 그렇게 함수형과 만남을 가지다보니 모나드라는 녀석의 존재를 알게됐고, 모나드의 늪에서 헤어나오지 못하는 중이다.
자, 그래서 모나드란 무엇인가? 하고 검색했더니 다음 세 가지 조건을 충족해야 한다고 한다.
1. 중립원 neutral element으로서의 return
2. 모나딕 합성
3. bind 결합법칙
Haskell(모나드는 기본적으로 Haskell이라는 함수형 프로그래밍 언어에서 시작한 것 같다.)을 슬슬 공부하면서 이해를 시도했지만,
뭔가 결합을 하고 타입에 영향을 받지 않는다
까지만 이해하고 더 이상은 외계어였다. ( 기본적으로 하스켈이란 언어를 어느 정도 수준으로 이해해야 이해가 가능한 단계였을 것 같다. )
하스켈 모나드를 공부하다 보면 아래와 같은 코드를 만나게 된다.
m >>= g = case m of
Nothing -> Nothing
Just x -> g x
무슨 말인고 하니 대략 m이라는 값이 없으면 아무것도 하지 말고, m이라는 값이 있으면 g(x)를 실행하라는 말이었다. ( 여기까지 이해한게 훌륭하다. )
그리고 그 다음 단계부터는 눈에 잘 안들어왔다.
아무튼 일단 여기까지 보니 뭔가가 하나 떠올랐다.
며칠 전에 앱 프론트 단을 계발하며 api 작업을 할 때, js에서 optional chaining을 사용했던 기억이 떠올랐다.
이런 식이었다.
key = person?.[0] /// person이라는 값이 있어? 있으면 person[0]을 키라는 변수에 넣어달라.
아주 유용했다.
API 작업을 하다보면 값이 안 날라올 때가 있고, 그럴때마다 에러를 처리해주려면
person === undefined ? null : person[0]
와 같이 처리해주어야 했기 때문이다. 어쨌든. 옵셔널 체이닝과 위 모나드의 조건이 어느 정도 맞아 떨어지는 것 같지 않나 하는 생각이 들었다.
사실 모나드를 설명하는 자료는 많았다.
기본적으로 Haskell로 설명하는 자료부터 시작해서, javascript(typescript를 가미한...)까지..... 그러나 읽으면 읽을 수록 미궁으로 빠지는 건 어쩔 수 없다.
그러다가 다음 자료를 접했다.
Nikolay Grozev : https://nikgrozev.com/2013/12/10/monads-in-15-minutes/
Thank you Nikolay.
익숙한 언어인 파이썬을 활용한 접근이었다.
def f1(x):
return (x + 1, str(x) + "+1")
def f2(x):
return (x + 2, str(x) + "+2")
def f3(x):
return (x + 3, str(x) + "+3")
f1,f2,f3 함수는 각각 전달 받은 값 x에 +1,+2,+3 한 값과 "전달받은 값+1","전달받은 값+2","전달받은 값+3"이라는 두 값을 결과값으로 return 한다.
=> return한 값의 [0] 은 숫자형이고, [1]은 문자열형이다.
아주 퓨어한 함수형 코드라고 할 수 있겠다.
log = "Ops:"
res, log1 = f1(x)
log += log1 + ";"
res, log2 = f2(res)
log += log2 + ";"
res, log3 = f3(res)
log += log3 + ";"
print(res, log)
완벽한 방법은 아니다.
f4라는 함수를 추가하게 된다면 그에 맞춰서 고정적인 코드 를 반복해서 넣어주어야 하기 때문이다. 사실 필자는 위 코드도 처음 보고 이해하는데 시간이 조금 걸렸다. 그 만큼 같은 형식의 함수가 계속해서 추가된다면, res와 log와 관련하여 읽기 쉬운 코드라고 할 수는 없을 듯 하다.
함수를 연결한다고 했을 때 떠오르는 가장 좋은 모습은 아마 이게 아닐까 한다.
f3(f2(f1(x)))
그러나 이러한 형태의 함수의 문제점은 파라미터와 리턴값 이다.
각 함수의 파라미터는 하나의 값만 받는데, 리턴값은 두 개이기 때문이다. 이에 우리는 함수와 함수를 결합할 때 중간에서 뭔가 해결을 해줄만한 녀석이 필요하다.
def unit(x):
return (x, "Ops:")
def bind(t, f):
res = f(t[0])
return (res[0], t[1] + res[1] + ";")
unit이라는 함수와 bind라는 함수를 준비해보았다. 위 함수들에 대한 설명을 하기 전에 활용 결과부터 보면 이렇다.
bind(bind(bind(unit(x), f1), f2), f3)
자 그럼, x = 0 을 넣었을 때를 가정하고 어떤 논리로 위 코드과 우리가 의도 했던f3(f2(f1(x))) 이 식이 같은 역할을 하는지 확인해보자.
최종 결과값: (6,"Ops:0+1;+2;+3;")이라고 볼 수 있다.
자, 이제 unit과 bind라는 함수 덕에 우리는 새로운 함수 f4,f5,.....fn을 만들 경우, 글루 코드를 반복해서 넣지 않고 위와 같은 형태로만 넣어주면 된다.
def f1(x): return x + 1
def f2(x): return x + 2
def f3(x): return x + 3
위와 같은 함수들이 있다. 이번에는 f3(f2(f1(x)))와 같은 식을 그대로 구현해 x+1+2+3의 값을 구할 수 있다.
그런데 이번에는 x, x+1, x+1+2, x+1+2+3의 값도 같이 구해보고자 한다
lst = [x]
res = f1(x)
lst.append(res)
res = f2(res)
lst.append(res)
res = f3(res)
lst.append(res)
print(res, lst)
위 상황과 같다. 새로운 함수 f4,f5,,,,fn을 추가할 때마다 글루코드를 반복적으로 추가해주어야 한다.
def unit(x):
return (x, [x])
def bind(t, f):
res = f(t[0])
return (res, t[1] + [res])
위 함수와 함께라면 우리는 글루 코드를 반복해서 추가할 필요없이 아래와 같이 해결할 수 있다.
bind(bind(bind(unit(x), f1), f2), f3)
아주 좋다. unit이라는 함수와 bind라는 함수의 역할을 조금은 이해하겠다.
unit은 파라미터를 일정한 자료형에 맞추어 돌려주고, bind 함수는 특정 값과 함수를 파라미터로 전달받아 전달 받은 특정 값을 전달 받은 함수를 통해 계산했다.
조금 더 정제해서 정리하면 아래와 같다고 할 수 있지 않을까
class Employee:
def get_boss(self):
# Return the employee's boss
def get_wage(self):
# Compute the wage
위와 같은 클래스에 대하여 각각의 Employee는 Employee타입의 boss와 a wage 값을 가지고 있고, 각각의 값은 위 메소드를 통해 접근할 수 있다. 이때, boss가 없거나 a wage를 알 수 없을 경우 None 값을 가질 수 있다.
이러한 프로그램을 우리는 다음과 같이 업데이트 할 것이다.
1. John이라는 instance가 자신의 boss의 wage를 반환한다.
2. 만약, wage가 아직 결정되지 않았거나, John이 없을 경우 None을 반환한다.
자 이와 같은 결과를 얻기 위해서 우리는 아래와 같은 방법을 생각할 수 있다.
john.get_boss().get_wage()
result = None
if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None:
result = john.get_boss().get_wage()
print(result)
양심상 반복적인 요소(get_boss())와 같은 것들을 한 번만 호출하는 형식으로 코드를 변형하면 아래와 같다.
result = None
if john is not None:
boss = john.get_boss()
if boss is not None:
wage = boss.get_wage()
if wage is not None:
result = wage
print(result)
이쯤되면 이제 예측이 간다. 우리가 뭘 해야할지.
unit과 bind가 필요한 시점이다.
def unit(e):
return e
def bind(e, f):
return None if e is None else f(e)
그리고 다음과 같이 호출하는 것이다.
bind(bind(unit(john), Employee.get_boss), Employee.get_wage)
자 이제 간단해졌다.
그리고 지난번에서 조금 더 발전하여 bind 함수에서 특정 값을 함수에 넣어 실행할 뿐만 아니라 특정 값이 None인지 아닌지 체크까지 한다.
그리고 매번 함수를 실행하며 None값을 찾으면 에러를 발생시키지 않고 None값을 반환한다.
(여기까지 오니 더욱더 Optional Chaining이 모나드라는 개념에 포함된다는 확신이 든다.)
먼길 돌아왔다.
결국, Nikolay 선생님께서 하시는 말씀은 이것이다.
모나드는 unit(파리미터를 조절함으로써 시작자 역할을 수행)과 bind(글루 코드 역할을 수행)라는 함수를 사용한 패턴이라는 것이다.
여기까지 알려주시는 것으로 모자르다 생각하셨는지 선생님께서 우리가 bind로 연속적으로 사용하실까봐 다음과 같은 꿀팁으로 마무리 하셨다.
def pipeline(e, *functions):
for f in functions:
e = bind(e, f)
return e
자, 이제 우리는 더 이상 bind 함수마저 반복적을 쓸 필요없이
ipeline(unit(x), f1, f2, f3, f4)
이렇게 쓰면 끝이다.
다시 한 , Nikolay 선생님께 감사말씀드리며 Monad에 대한 문을 열어보았다.
Thanks to Nikolay (https://nikgrozev.com/2013/12/10/monads-in-15-minutes/)