왜 모나드를 사용해야 하는가 - ChoDragon9/posts GitHub Wiki
이 글은 weirdmeetup/translate 이 저장소에서 가져왔다. 콘텐츠가 너무 좋아 혹시나 삭제되면 다시 못볼거 같은 불안감에 카피해왔다.
왜 모나드를 사용해야 하는가
Original: https://cdsmith.wordpress.com/2012/04/18/why-do-monads-matter/
(노트: 이 내용을 구체화하기 위해 일주일을 고민했다. 우연히 좋은 친구이자 하스켈 프로그래머인 Doug Beardsley도 모나드에 관한 두 가지 글을 이번 주말에 작성했다. 이런 우연이! 하지만 초조하지 않아도 된다. 이 글은 다르다. 하스켈 프로그래머가 어떻게 모나드를 사용하는지에 관해 가르치려고 쓴 글이 아니다. 이 글은 모나드와 같은 개념과 통찰이 어떻게 나타났는지 거슬러 올라간다. 하스켈로 어떻게 프로그램을 작성하는가에 대해서는 거의 찾을 수 없을 것이다.)
소프트웨어 개발을 위한 범주론 Category Theory
당신이 소프트웨어 개발자 라면 모나드(monads)에 관해 들어봤거나 무엇인지 궁금했던 적이 있는가? 하스켈을 배우려고 시도해보고 그 어려움에 고생해본 적이 있는가? 사람들이 왜 이런 것들을 고민하는지 의문이 들지 않았나? 마이크로소프트 "Channel 9"에서 마이크로소프트 사람들이 이야기하는 것은 들어봤지만 개발을 매일 하면서도 관련된 문제를 해결해본 적이 없었을 지도 모른다.
만약 수학에 관심이 있다면, 왜 컴퓨터 과학을 연구하는 사람들이 범주론에 흥미를 느꼈는지에 대해 들어본 적이 있나? 명확하게 왜, 어떤 문제로 이런 문제에 흥미를 가지는 지 살펴본 적이 있는가? 이 모든게 정말 진실이라면? 당신이 대수학자에 딱딱 맞는 질문만 하는 내 친구와 같은 사람이라면, 1990년대 컴퓨터 과학과 범주론에 대한 엄청난 흥분을 들어본 경험이 있는가? 그 이후로 성공했는지, 망했는지 들어본 적이 있는가?
이 글은 다음과 같은 질문에서부터 시작하려고 한다. 다음의 내용을 세세한 내용과 예제로 증명하려고 한다.
- 범주 기반의 통찰과 개념이 컴퓨터 프로그래밍으로 어떻게 오게 되었는지 (세부적으로 모나드와 같은 부분들)
- 왜 미래의 프로그래밍은 이런 개념에 대해 거짓말을 하고 현대 주류인 언어에서까지 누락해서 우리가 비싼 비용을 치루게 하고 있는지
- 컴퓨터 프로그래밍에서 범주 기반의 개념을 적용해 문제를 해결한다면 어떤 기술적 상태가 되는지
범주론에 대해 알지 못하고 있더라도 전혀 두려워 할 필요 없다. 범주에 대한 개념을 친절하게 설명할 것이고 모나드가 어떤 것인지 찾을 수 있을 것이다. 하지만 조금 속도를 줄이고 카테고리의 정의와 함수의 합성과 같은, 연관된 개념을 먼저 이해하는 시간을 갖길 바랄 것이다. 이점은 아주 중요하다. 그런 후 "이것이 모나드랑 어떻게 동작하는가?" 부분으로 건너뛰어 전통적으로 수학에서 이야기하는 모나드의 의미를 살펴보고 싶을지도 모른다. 하지만 그럴 필요 전혀 없으니 걱정하지 말자.
반면에 당신이 수학자라면 범주론의 기초를 살펴보는 부분을 건너뛰어 컴퓨터 프로그래밍 관점에서 이야기 하는 부분을 확인하고 싶을 것이다. 미리 주의를 준다면, 이 글에서는 Kleisli 카테고리를 통해 진행할 예정이다. 그 부분까지 설명할 동안 이 관계가 어떻게 동작하는지 잘 알아두면 좋겠다.
준비 됐으면 시작하자!
컴퓨터 프로그래밍과 함수: 미묘한 관계
기습 문제: 컴퓨터 프로그래머는 함수를 사용하는가?
알고 있는 모든 컴퓨터 프로그래머에게 물어보면 다음과 같은 대답을 듣게 된다. "그렇다!" 함수는 컴퓨터 프로그래머가 사용하는 가장 기초적인 도구 중 하나다. 상대는 그렇다고 대답하고는 이렇게 실없는 질문을 하는 사람이 있나 하고 쳐다볼 것이다. 물론 컴퓨터 프로그래머는 함수를 사용한다. 이건 마치 목수보고 못을 쓰냐고 물어보는 것이랑 다름 없다.
하지만 진실은 조금 복잡하다. 수학자에게 함수는 입력값과 출력값의 덩어리고... 그게 전부다! 두 개의 함수가 각각 같은 입력값과 같은 출력값 덩어리를 가지고 있다면 그 두 함수는 같은 함수다. 함수는 공식이 될 수도 있고, 입력과 출력에 관한 표가 될 수도 있고, 그래프와 같이 실수의 범위가 될 수 있다. 하지만 컴퓨터 프로그래머에게 함수의 예를 들라고 하면 앞서 이야기 한 것과 대조적인 정의를 들을 수 있다. 이걸 "미적분학 수업시간에 땡땡이 쳤어요" 함수라고 한다. 다음 항목에 대해 컴퓨터 프로그래머는 함수라고 부르고도 행복해하지만, 수학자에게는 전혀 함수가 아니다.
- 무작위로 선정된 숫자를 반환하는 "함수". 평가를 할 때마다 다른 답을 내놓음.
- 일요일에 하나의 답을 반환하는 "함수". 월요일에는 앞서 답과 다른 답을 내고, 화요일에 또 다른 답을 반환함.
- 컴퓨터 화면에 값을 계산하려고 하면 자주 나타나는 단어 "함수".
무엇이 어떻게 되고 있을까? 많은 컴퓨터 프로그래머는 위 목록에 나오는 함수를 함수라고 부르고 행복하게 지낸다. 하지만 실제와는 무언가 다르다. 잠깐만! 그래도 여기서 함수의 일반적인 부분을 확인할 수 있다. 다시 말하면, (a) 매개변수, 즉 정의역(domains)을 나타내고, 그리고 (b) 값, 즉 치역(ranges)을 나타낸다. (컴퓨터 프로그래머 대다수는 매개변수가 없는 함수, 또는 반환하는 값이 없는 함수에 대해 이야기하는 것도 좋아한다. 여기서 그렇게 까탈스러울 필요는 없다. 여기서는 각각의 요소 묶음으로 정의역과 치역을 가지고 있다는 점 말고는 실질적인 정보가 없지만 함수가 어떤 형태인지 가늠할 수 있다.)
더 중요하게도, 수학자의 "함수"에서는 요소 하나가 더 있다. 바로 함수는 합성이 가능해야 한다. 즉, 함수에서 나온 결과를 다른 함수의 매개변수로 사용하는 것이 가능해야 한다. 여기서 말하는 합성은 수학에서 기본적으로 이야기하는 함수의 합성을 얘기하는 것이다. (f · g)(x) = f(g(x)). 사실 모든 "함수"는 다른 함수와 합성하기 위해 존재한다! 초기 컴퓨터는 컴퓨터 메모리 내에서 접근 가능한 공간에 붙어있는 정보를 계속 확인하는 것이 전부였다. 하지만 뿔뿔이 흩어져있는 정보를 한곳에 모아 쉽게 확인하기 위한 방법이 필요했기 때문에 함수와 함수의 합성이란 방식을 사용하게 되었다.
지금까지의 내용을 정리하면,
- 컴퓨터 프로그래머가 말하는 함수는, 엄밀히 수학자가 이야기하는 함수가 아니다.
- 입력(정의역)과 출력(치역)을 가지고 있고, 합성을 할 수 있으면 함수라고 한다.
범주를 따라서
이전 섹션에서 함수에게 필요한 모든 부분을 정리했다. 함수는 정의역과 치역, 그리고 합성이 가능해야 한다. 하지만 동시에 이것은 수학에서의 함수가 아니다!! 당황스럽나? 농담이다. 수학자는 이런 문제를 아주 많이 마주한다. 그래서 함수 형태에 대한 명확한 명칭을 갖고 있다. 그 명칭이 바로... 기대하시라... 범주다!
수학에서의 범주는:
- "객체 objects"의 집합 (_set_으로 볼 수 있음)
- "화살표 arrows" (집합(set) 사이의 함수라고 볼 수 있음)
- 정의역과 치역을 가리키는 두 화살표
- 각 객체가 가지고 있는 "항등 identity" 화살표 (항등 함수를 생각하자. f(x) = x)
- 한 화살표의 치역과 다른 화살표의 정의역이 같다면 그 둘을 합성할 수 있음
무엇을 범주로 부를 것인지 동의하기에 앞서, 몇가지 규칙이 필요하다. 예를 들면, 항등이 포함된 어떤 함수든 합성을 해서 그 결과가 실제로 변하지 않는다면 합성된 함수는 결합법칙을 준수하게 된다. 이런 과정이 이상하게 보일지 몰라도 놀라지 말자. 시간을 들여 연필을 잡고 앞서 했던 함수 합성의 정의를 적어 단순화 해보자. (f · g)(x) = f(g(x))
범주의 장점을 살펴보자. 범주는 단지 수학자가 만들어낸, 의미 없는 추상적 개념이 아니다. 범주는 문자 그대로 수백 가지의 정의역, 치역, 그리고 합성으로 구성된 함수를 표현하기 위한 방법이다. 대수학이라면 군, 환, 그리고 벡터 공간, 기하학이라면 거리공간과 위상공간, 수열이라면 부분 순서 집합과 그래프 경로, 논리학이라면 명제와 논거 같은 것이다. 범주를 통해 이 수학 개념 대부분을 설명할 수 있다. 정리하면, 범주는 정의역과 치역을 구성하는 것을 다루기 위한 알맞은 직관으로 현재 우리 상황에 정확하게 필요한 것이다.
네 명의 기수가 만드는 재앙
이제 왜 범주가 나왔는지 확인할 수 있다. 범주는 함수가 아니지만 함수처럼 구성할 수 있다. 그러나 단순히 범주가 존재한다고 해서 여기서 다룰 만한 가치가 있는 것은 아니다. 실제로 가치가 있는 부분은 범주와 관련한 개념이 단순히 개념에 그치는 것이 아니라 컴퓨터 프로그래머가 겪는 일반적인 문제를 표현하는 방법이기 때문이다.
이제 더 상세한 내용과 함께 예시를 소개해 탐험을 위한 이 여정의 방향을 설명해야 할 때다. 여기서는 수학에서 이야기 하는 함수가 아닌 컴퓨터 프로그래머가 일반적으로 이야기하는 함수를 각각의 예시로 다룰 것이다. 이 예는 실제 컴퓨터 프로그래머가 겪고 해결할 만한 실제적인 문제를 반영하며 이 기법에 대한 실제적인 측면은 나중에 깊이 있게 다루고자 한다. 현재는 일반적인 개념과 친해지는 것으로 충분하다.
첫번째 기수 : 실패(Failure)
첫번째 문제는 실패다. 컴퓨터 프로그래머는 엄청 많은 일을 하고 그 와중에 실패가 발생한다. 파일을 읽는 과정에서 파일이 존재하지 않을 수도 있고, 하나의 컴퓨터가 여러 사용자를 가지고 있어 해당 파일을 읽을 권한이 없을 수도 있다. 인터넷을 가정하면 네트워크가 문제 있거나 지나치게 느릴 수 있다. 단순히 간단한 계산을 한다고 해도 그 데이터가 너무 크면 메모리 범위를 넘어가는 등 문제가 발생할 수 있다. 이런 이유로 실패를 다루는 것은 항상 신경써야하는 문제다.
일반적으로, 현대적인 컴퓨터 프로그래밍 도구에서는 함수가 실패할 수 있다는 사실을 항상 감안한다. 함수의 결과로 답을 얻을 수 있고 반대로 일감을 완료하지 못하면 그 이유가 무엇인지 알 수 있다. 이런 일이 발생하면 프로그래머는 그 일에 대해 확실히 처리해야 할 책임이 있다.. 공간이 비었다는 것을 다른 사람이 알 수 있도록, 절반만 완료된 일로 만들어진 찌꺼기 덩어리를 컴퓨터 메모리에서 치워야 하며 사용했던 부분들을 원래 자리로 돌려놔야 한다. 프로그래머가 반복적으로 나타나는 실패에 대해 쉽게 처리할 수 있도록 도와주는 것은 프로그래밍 기술 또는 툴에서 제공해야 할 중요 요소 중 하나다.
두번째 기수 : 의존성(Dependence)
두번째 문제는 정보 바깥에서 나타나는 의존성이다. 수학에서의 함수는 친절하고 독립적이지만 컴퓨터 프로그래머에게는 그런 안락함이 없다. 컴퓨터 프로그램은 환경설정 덩어리다. 간단한 휴대폰조차도 페이지와 페이지 설정이 있다. 사용자가 사용하는 언어는 무엇인가? 얼마나 자주 복사를 하는가? 네트워크를 사용하는데 암호화를 할 것인가? 오늘날의 어플리케이션 중 "설정"이나 "환경설정"을 메뉴 항목으로 가지고 있지 않는 경우는 드물다. 다른 맥락에서도 동일하다. 컴퓨터 프로그램은 일종의 "일반 상식"인 정보에 의존하고 있고, 이 정보를 어플리케이션 전체나 부분에서 필요로 한다.
이 문제를 해결하기 위한 방법은 시간에 따라 발전했다. 모두가 쉽게 확인할 수 있도록 잘 알려진 메모리 위치에 저장되어 있다고 한다면, 필요한 정보가 있을 때 쉽게 접근해서 열어볼 수 있을 것이다. 하지만 문제는 프로그램이 필요로 하는 정보가 다른 곳에 있어 그 정보를 살펴보다 프로그램끼리 서로 문제를 발생시킬 수도 있다는 점이다. 엄청난 영향력을 주는 기법 중 하나인 _객체지향 프로그래밍_은 이 문제를 함수를 하나의 맥락에 의존하는 정보와 함께 묶는 방법을 사용한다. 가장 간단하고 유연한 답은 모든 함수에 필요한 정보를 넘겨주는 방법이지만, 엄청나게 많은 위치 정보를 매개변수로 함수를 호출할 때마다 매번 넘겨주려면 아주 아주 불편할 것이다.
세번째 기수 : 불확실성(Uncertainty)
세번째 문제는 불확실성으로 비결정론(non-determinism)으로도 알려져 있다. 일반 함수는 입력과 출력이 결합되어 있다. 비결정적 함수는 입력에 대해 여러개의 가능한 출력이 존재한다. 비결정론은 앞서 두가지 문제에 비해 덜 알려져 있지만 분명 해결해야 할 문제 중 하나다. 잘 알려지지 않은 이유는 일반적인 용도의 언어에서는 납득할 만한 해결책이 나오지 않았기 때문이다. 다음을 고려하자.
- 이론적인 컴퓨터 과학은 비결정론에 대해 매번 시간을 할애한다. 이 방법이 데이터 파싱부터 검색, 검증까지의 많은 계산 문제를 논증하기 위한 옳은 접근 방법이기 때문이다. 물론 비결정론은 꼭 프로그래밍 실행을 위해서 만들어진 말은 아니다.
- 비결정론은 요청하거나, 검색하거나, 다양한 답이 가능한 상황에 나타난다. 이런 작업은 특정 분야에서 사용하는 여러 언어에 의존하고 있다. SQL부터 Prolog, 그리고 가장 최근에 나타난 언어 통합적 기술인 LINQ도 있다.
- 언어 장벽을 넘어가는 것이 가치가 있지 않을 때에도 통제 가능한 구조체로 만드는 것이 가능함을 보여주기 위해 대형 쿼리와 검색 작업에 특화된 언어에서 조차도 여전히 중첩된 반복문과 수많은 코드를 이용한다. 이런 과정으로 만들어진 코드는 더욱 더 복잡한 코드 구조에 의지하게 된다는 것을 확인할 수 있을 것이다.
앞서 얘기한 실패와 의존성에 대해서, 현재 주류 언어는 적어도 부분적인 해결책을 가지고 있는 반면에 비결정론에 대해서는 특수 목적의 대안 언어말고는 해결하지 못하고 있다. LINQ와 같이 주목할 만한 예외를 제외하고는 말이다.
네번째 기수: 파괴성(Destruction)
마지막으로 네번째 문제는 파괴성이다. 수학적 타입의 함수를 평가하면 결과를 통해 관찰하기(observable)만 가능하다는 점은 이미 알고 있을 것이다. 하지만 컴퓨터 프로그래밍에서 함수는 영구적인 효과를 야기하는 경우가 있다. 정보를 화면에 표시하거나, 다른 컴퓨터 또는 다른 사람의 응답을 기다리거나, 문서를 출력하거나, 만약 국방 시스템이라면 문자 그대로 뭔가 폭파 한다거나 말이다. 평가하는 중에 순서가 바뀌는 등, 수학에서 명확하게 지정되지 않은 점들로 인해 꽤 많은 문제가 발생할 수 있다.
파괴적인 세계에서의 컴퓨터 프로그래밍 함수는 매우 중요하다. (여기서 파괴적인 세계란 되돌릴 수 없는 영향을 가졌다는 뜻으로 사용했다.) 이 특징으로 인해 프로그래밍에서 에러가 더 쉽게 발생한다. 또한 각각의 작업을 분리해서 동시에 실행하는 것이 더 어려워진다. 예를들어, 현대적인 멀티코어 컴퓨터를 갖고 있다면 이런 동시성을 기대하겠지만 나누어진 작업들의 순서가 잘못되면 정상적으로 동작하지 않기 때문에 문제가 발생하게 된다. 하지만 동시에 이 파괴적인 효과는 컴퓨터 프로그래밍에서 가장 중요한 관념이다. 프로그래밍이 관찰자 효과(관찰로 인해 결과에 영향이 있는 효과)가 없다면 구동할 의미가 없기 때문이다. 그래서 실질적으로 모든 주류 프로그래밍 언어에서의 함수는 파괴성의 문제를 잘 다룰 필요가 있다.
함수로 돌아와서
컴퓨터 프로그래밍 세계에서 발생하는 여러 문제를 찾아 직접 확인했다. 우리는 실패할지도 모르고, 의존성으로 인한 외부의 맥락을 해결해야하고, 비결정론적 선택을 하며, 연산을 수행할 때 가끔씩 관찰자 효과를 가지기도 하는 소프트웨어를 개발한다.
멋지고 똑똑한 수학적 함수의 세계를 뒤로 미뤄둔 것처럼 보이지만 그렇지 않다! 눈을 가늘게 뜨고 자세히 보면, 사실 각각의 준(準) 함수(quasi-function)들은 -- [위의 네 문제점을 말한다] -- 믿을만하고 간단한 함수처럼 보이는 것을 알게 될 것이다. 물론 시간이 들겠지만 말이다. 이 준 함수들을 실제 함수로 변경하기 위해서는 함수의 치역을 다른 것으로 변경해야 한다. 각각의 상황에서 어떻게 함수의 타입을 변경해야 하는지 확인해 보도록 하자.
함수로 실패 다루기
의사(pseudo) 함수로 작성하는 첫 예제는 실패할 수 있는 함수다. 실패하는 함수는 단지 다음 두가지의 결과를 포함하고 있는 함수이기 때문에 쉽게 이해할 수 있다.
- 성공, 의도된(가능한) 결과를 반환
- 실패, 왜 실패했는지에 대한 설명을 반환
그래서 어떤 집합 A에서 새로운 집합 _Err(A)_를 정의하면, 집합 A에서 실패한 경우에 대한 이유를 포함하는 집합이다. 정의역 _A_와 치역 _B_를 갖는, 실패할 가능성이 있는 함수는 정의역 _A_와 치역 _Err(B)_를 갖는, 일반적인 함수로 볼 수 있다.
함수로 의존성 다루기
두번째로 작성할 의사 함수는 환경설정이나 어플리케이션 설정과 같이 함수 외부의 정보에 의존적인 경우를 다루는 함수다. 앞에서 작성한 방법과 비슷한 요령으로, 집합 _A_에서 집합 _Pref(A)_를 A 집합의 어플리케이션 설정 집합으로 정의한다. 자세히 보면 정의역 A, 치역이 _B_를 갖는 함수의 맥락으로, 정의역 A, 치역 _Pref(B)_인 평범한 함수가 된다. 다시 말하면, 먼저 집합 _A_로부터 값을 받는다. 그 값을 어플리케이션 설정이 담겨있는 다른 함수에 넣어 집합 _B_를 결과로 얻는다.
이 얘기가 혼란스러울지도 모른다. 치역이 다른 함수는 단순히 매개변수가 두 개인 함수 하나에 불과하다. 단지 이 매개변수를 한번에 입력해야 할 뿐이다. 시간을 들여 이 내용을 이해해야 한다. 이 동등한 두 개념 사이의 전환은 때로 "커링 currying" 이라고 불린다. 함수의 치역을 변경하는 것은 새로운 매개변수를 추가하는 것과 같은 효과를 갖고, 그렇게 추가된 매개변수를 통해 어플리케이션 설정을 받을 수 있게 된다. 불편하긴 하지만(나중에 처리할 것이다) 소원대로 의존적인 정보를 얻었다는 점을 기억하자.
함수로 불확실성 다루기
이 함수는 아마도 모든 예 중 가장 명확할 것이다. 세번째 타입은 비결정론을 표현한다. 즉, 명확한 답 하나가 아니라 여러가지 가능한 답이 존재하는 경우다. 충분히 쉽게 정리할 수 있다. 각각의 집합 _A_에서 _P(A)_를 멱집합으로 정의하자. 멱집합은 집합 _A_의 모든 부분 집합을 모은 집합을 뜻한다. 정의역 A, 치역 _B_를 갖는 비결정론적 함수는 정의역 A, 치역 _P(B)_를 갖는, 일반적인 함수가 된다.
함수로 파괴성 다루기
마지막으로 파괴적인 효과가 있는 함수를 다룬다. 여기서는 새로운 치역을 조금 더 정교하게 만들어야 한다. 치역 집합 _A_에 대해 _IO(A)_를 정의한다. (입력/출력을 뜻하며, 다른 세계와의 상호작용할 때 발생하는 영향을 개념화한 것이다.) 집합 _IO(A)_의 요소는 집합 _A_의 요소를 얻기 위해 해야 할 일을 알려주는 설명서에 해당한다. 이 요소는 집합 _A_의 원소는 아니지만, 드물게 집합에 포함되는 경우도 있고 그로 인해 관찰자 효과가 여러번 발생할 수 있다.
이제 앞서와 동일한 트릭으로 치역을 변경하려고 한다. 정의역 _A_와 치역 _B_를 갖는 파괴적인 함수는 정의역 _A_와 치역 _IO(B)_를 갖는, 낡고 평범하며 일반적인 수학 함수로 바꿀 수 있다. 다시 말해, 이 함수에 정의역 _A_를 주면, 이 평범하고 오래된 함수는 사실 치역 _B_를 어떻게 얻어야 할 지 모르면서도, 반드시 그 _B_에 대해 얘기해줄 수 있는 것이다.
하지만 함수의 합성은 어떻게 할 것인가? 이제 평범한 함수의 세계로 돌아와야 할 때인데 이 글의 처음 부분에서 무엇을 배웠는지 기억하는가? 우리가 함수를 좋아하는 이유는 함수의 합성을 좋아하기 때문이다. 하지만 이젠 합성을 잃어버린 것 같다. 정의역 A, 치역 _B_를 갖는 함수가 실패할 가능성이 있다면, 또 다른 정의역 _B_에서 치역 _C_의 함수가 있다면, 이를 정의역 _A_와 치역 _Err(B)_를 갖는 함수, 정의역 _B_와 치역 _Err(C)_를 갖는 함수로 바꿀 수 있다는 것을 알게 되었다. 하지만 이 두 함수의 정의역과 치역은 서로 맞지 않기 때문에 함수를 합성할 수 없다.
아, 안돼...
달리는 말을 멈춰 세워라. Heinrich Kleisli이 바로 탈출구!
사실, 함수의 특징을 모두 잃어버린 것이 아니다. 단지 이 "특별한" 함수를 어떻게 합성하는지 알려주지 않았을 뿐이다.
왜냐하면 이 방법은 어떤 수학 덕후가 먼저 찾았기 때문이다. 그 덕후가 여기서 만든 "특별한" 함수를 Kleisli 화살표라고 이름을 붙였다. 여기에 두가지 일이 한번에 일어나고 있으니 눈을 크게 뜨자. 먼저 Kleisli 화살표는 그냥 평범하고 오래되고 낡은 함수일 뿐이지만 이상하게 생긴 치역을 갖고 있다. 처음부터 함수였기 때문에 함수로서 합성할 수 있을 것이고 그것 자체로 충분하다. 하지만 동시에 그 함수는 "특별하기 때문에" 위에서 살펴본 함수를 Kleisli 화살표로 합성할 수 있다.
앞서 결정했던 부분을 기억하는가? 합성에 대해 생각하는 올바른 방법은 범주에 대해 이야기하는 것이다. 집합은 범주이며 일반적인 함수의 합성을 원할 때는 문제가 없다. 하지만 이제는 새로운 종류의 범주를 필요로 하고 있다. 이 범주를 Kleisli 범주라고 부른다. 범주론에 대해 다룬 부분이 기억나지 않는다면 잠시 다시 보고 오기 바란다. 범주론의 정의를 위해 객체, 화살표, 항등성, 그리고 합성이 필요하다.
- 모든걸 단순화 하기 위해, 새 범주에 들어있는 객체는 동일하다: 무언가의 집합
- 이 범주에 있는 화살표는, 놀랍지 않게도, Kleisli 화살표다.
- 아직 항등과 합성에 대해 다루지 않았다. 다음에 살펴보도록 하자.
먼저 실패를 살펴보자. 정의역 _A_에서 치역 _B_를 갖는 실패 Kleisli 화살표, 그리고 정의역 _B_에서 치역 _C_을 갖는 Kleisli 화살표가 주어졌다. 이 두 화살표를 정의역 _A_에서 치역 _C_을 갖는 Kleisli 화살표로 합성하려 한다. 다시 말하면, 정의역 A 치역 _Err(B)_의 함수, 정의역 _B_와 치역 _Err(C)_를 갖는 이 평범한 두 함수를 합성해서 정의역 A 치역 _Err(C)_를 갖는 함수를 얻으려고 하는 것이다. 어떻게 하면 되는지 가만 생각해보자.
에러를 다루는 아이디어의 중심은 첫번째 함수에서 오류를 반환하면 거기에서 멈추고 오류를 보고해야 한다는 점이다. 첫번째 함수가 성공적으로 실행하면 다음 두번째 함수도 실행하고 거기서 나온 결과를 반환해야 한다. (그 두번째의 결과가 오류든 성공했든 상관없이 말이다.)
정리하면:
- 만약 _g(x)_가 오류라면, (f · g)(x) = g(x)
- 만약 _g(x)_가 성공했다면, (f · g)(x) = f(g(x))
범주의 정의를 끝냈으니 Kleisli 화살표의 항등성도 결정할 필요가 있다. 위 정리에서 하나가 실패하면 아무 일도 하지 않기 때문에, 어떤 종류의 Kleisli 화살표를 합성하더라도 다른 하나를 변경할 필요가 없다. 이 함수의 항등성을 정의역 A, 치역 Err(A) 함수로 생각하면, 마치 평범한 집합과 같이 함수 f(x) = x 에 불과하다. 그 뜻은 절대 오류를 반환하지 않는다는 것 즉, 결과를 성공적으로 반환하기만 한다는 뜻이다.
남은 세가지 예를 좀 더 자세하게 설명하려고 한다. 이게 어떤 방식으로 동작하는지 아직 명확하게 이해가 되지 않을 수 있다. Kleisli 화살표의 범주가 어떤 것인지 정의를 이해하는데 도움이 되도록 차근차근 살펴보려고 하니 희망을 잃지 말자.
다음은 의존성을 위한 Klesli 함수 즉, 정의역 _A_와 치역 _Pref(B)_를 갖는 함수를 살펴보자. _Pref_를 치역에서 사용하기 위해 호출하는 것은 어플리케이션 환경설정을 위해 새로운 매개변수를 추가하는 것과 동일하다. 가장 중요한 부분은 두 함수가 필요로 하는 부분이 어플리케이션 환경설정이라면 두 함수에 동일한 환경설정을 넣으면 되지 않을까. 그렇다면 두 함수를 Kleisli 화살표로 합성하는 것은 추가적인 환경설정 매개변수를 포함하는 새로운 함수가 된다. 즉 하나의 값을 두 컴포넌트 부분에 넣는 함수가 된다. Kleisli의 항등성은 추가적인 환경설정 매개변수를 받지만 그 입력값과 상관 없이 입력값을 반환할 것이다.
불확실성, 또는 비결정론을 위한 Kleisli 화살표는 정의역 _A_와 치역 _P(B)_를 갖는 함수로 _P(B)_는 _B_의 멱집합이다. 비결정론에서의 주요 개념은 각각의 단계에서, 현재 가능한 모든 값을 시도해 모든 결과를 수집하기를 원한다. 그래서 각각의 가능한 결과를 합성하게 될 때, 첫 함수에서 가능한 모든 결과가 나온 후에 계산되는데 그 가능한 결과는 모든 값의 합집합으로 귀결된다. 물론 항등성은 실제로 비결정론적인 것은 아니며, 입력값을 하나의 원소로 가진 집합으로 반환하게 된다.
마지막으로 파괴적인 함수를 위한 Kleisli 화살표 즉, 정의역 A, 치역 _IO(B)_를 갖는 함수다. 여기서 중요한 아이디어는 각각의 단계별로 따라야 할 지시를 조합하는 것이다. 먼저 하나를 하고 다음을 처리한다. 그래서 함수의 합성에서는 첫번째 행동에서 해야 할 일을 지시해 실행한다. 그 결과로 두번째 행동을 살펴본 후, 두번째 행동을 실행하는 순서로 진행된다. 여기서 Kleisli의 항등성은 지시는 아무 행동도 하지 않고 입력이 그 결과로 전달된다. 여기까지 4가지의 흥미로운 예제를 통해 새로운 범주, 즉 Kleisli 범주를 만들었다.
이 새로운 범주는 함수와 같이 행동하는 것으로 합성, 항등성과 같은 연관 개념을 사용할 수 있게 만들어 각각 특정 문제의 특별한 성질을 표현할 수 있게 되었다. Kleisli 범주를 활용해 만든 합성에 대한 관념은 오랫동안 컴퓨터 프로그래밍에서 문제였던 부분을 멋지고 조화롭게 해결할 수 있게 한다.
이것이 바로 왜 모나드를 신경써야 하는가에 대한 이유다.
엥 모나드?!? 그렇다. 우리는 모나드를 배웠다. 단지 모나드라는 말을 사용하는 것을 잊었을 뿐이다.
이것이 모나드랑 어떻게 동작하는가?
이 부분은 앞서 다룬 부분과 모나드가 수학적 이해에서 어떻게 연관성을 갖는가에 대해 다룬다. Wikipedia를 열거나, 범주론에 관한 대부분의 책에서 모나드에 대해 찾아보면 여기서 이야기 한 내용과는 사뭇 다를 것이다. 자기함자 Endofunctor와 두가지의 자연변환 natural transformation, 삼각형과 사각형을 오가는 프로퍼티를 확인할 수 있을 것이다.
물론 함자 functor에 관해서도, 자연변환 natural transformation에 관해서도 아직 다루지 않았다. 그걸 모르고도 모나드에 대해 배울 수 있을까? 사실 모나드에 대해 묘사하는 다른 방법이 있다. 바로 앞에서 살펴본 방식이 바로 그것이다. 앞서 만든 함수에서 치역을 변경해 만든 Err, Pref, P, 그리고 _IO_는 모나드의 예제다. 이 예제를 수학적으로 규정한 모나드가 맞나 확인하려면 꽤 복잡한 과정을 거쳐야 한다. 먼저 이 예가 함자인지 증명해야 한다. 그리고 _η_와 _µ_로 불리는 두 자연변환을 만든 후 이 둘이 자연적인지 증명해야 한다. 마지막으로 3가지의 모나드 법칙을 증명해야 한다.
잠깐, 더 쉬운 방법이 있다. 앞서 범주론에서 만났던 Heinrich Kleisli가 앞에서 만들었던 것과 같이 범주를 만들 수 있다면, 다시 말해 화살표가 단지 수정된 치역을 가진 함수를 의미한다면 그 범주는 모나드를 만족한다고 볼 수 있다. 그러므로 이제 수학적 아이디어로의 모나드를 생각하는 것보다 Kleisli 화살표에 대해서만 염두하는 것으로도 충분해 컴퓨터 프로그래머라면 이해하기 더욱 쉬운 개념이 되었다. 이 Kleisli 화살표는 함수를 조금 수정한 개념에 불과하다. 즉 범주론에 대해 듣기 전부터 오랜 기간 사용해왔던 그 함수라는 사실을 기억하자! Kleisli가 앞서 만든 Kleisli 화살표로 합성을 할 수 있는 것을 알려준 이후로 (다시 말해, 결합한 함수와 동일한 것처럼 행동하는 항등성) 모나드 증명을 위한 나머지는 이미 우리를 위해 자동으로 증명된 것이나 마찬가지다.
여전히 이 둘 사이의 관계에 대해 부가적인 관심을 가질 수 있다. 모든 상세한 내용을 알려주진 않겠지만, 범주론에 대해 익숙해 그 증명과 관련된 내용을 알고자 하는, 흥미롭게 생각하는 독자를 위해 어떤 구조인지 알려줄 것이다. 여기서 특정한 예제를 집어, _Err_을 모나드로 사용할 것이지만 _Err_에만 특정된 내용은 아니다.
-
이미 객체에서 객체로 작용하는(map) _Err_을 살펴보자. 하지만 전통적인 정의에서의 모나드는 함자 functor여야 한다. 그 말은 정의역 A 치역 _B_를 갖는 함수 f가 주어지면 정의역 Err(A) 치역 _Err(B)_를 갖는 Err(f) 함수를 만들 수 있어야 한다. 여기서는 underlying 범주론을 따라서, (Kleisli 범주론이 아니다. 단지 범주론의 집합이다.) 정의역 Err(A) 치역 _Err(A)_를 갖는 항등 함수를 찾는다. 그리고 정의역 _B _치역 _Err(B)_를 갖는 Kleisli 항등성을 찾는다. 이 Kleisli 항등성을 underlying 범주론에 함수 _f_와 함께 조립하면, 정의역 A 치역 _Err(B)_를 갖는 함수를 얻을 수 있다. 이로써 정의역 Err(A) 치역 _Err(A)_를 갖는 항등성과 정의역 _A_와 치역 _Err(B)_를 갖는 함수 둘을 가지고 Kleisli 합성을 수행해 _Err(A)_를 정의역으로, _Err(B)_를 치역으로 갖는 함수를 얻을 수 있다. 이것이 바로 _Err(f)_로 불리게 된다.
-
다음으로 항등성 함자를 정의역으로, _Err_를 치역으로 갖는 자연순환 _η_이 필요하다. 이 부분은 쉽다. Kleisli 항등성이 _η_를 구성하고 있다.
-
마지막으로 _Err²_를 정의역으로, _Err_를 치역으로 갖는 자연순환 _η_를 필요로 한다. _A_에서 _η_의 구성요소를 얻기 위해, underlaying 범주에서 정의역 Err(Err A) 치역 _Err(Err A)_을 갖는 항등 함수와 정의역 Err A 치역 _Err A_인 항등 함수를 얻는다. 그리고 이 둘을 Kleisli 합성을 통해 조립하면 정의역 Err (Err A) 치역 _Err A_를 갖는 함수를 얻을 수 있게 된다. 이 함수가 _η_의 구성요소다.
반대 방향에서 생성하는 것은 쉽다. 앞에서 제시된 모나드 _Err_은 _η_와 µ, Kliesli 범주에 의해 다음과 같이 생성된다.
- 항등성은 _η_의 구성요소다.
- 정의역 A 치역 _Err(B)_인 함수 f, 정의역 _B_와 치역 _Err(C)_인 함수 _g_가 주어졌을 때, 이 둘은 _µ · Err(g) · f_로 조립할 수 있다.
다시 말하지만, 모나드 정의에 대한 세세한 내용과 범주론 법칙은 독자에게 남겨둔다. 위 정리가 곁다리로써 유용했으면 한다. 다시 단어 "모나드"를 사용하는 것으로 돌아와 Kleisli 범주론을 통해 모나드에 대해 이야기하자.
모나드주의 혁명에 동참하라
여기서 잠시 정리해보자.
- 컴퓨터 프로그래머는 무언가를 조립하는데 그걸 함수라고 부른다.
- 명확히 말하면 그건 함수가 아니다. 그래도 범주를 만들긴 했다.
- 사실, 당신 눈이 잘못되었거나, 이상한 치역을 갖고 있다고 가정하면, 함수라고 볼 수 있다.
- 그 함수가 구성한 범주는 Kleisli 범주로 불리고 이는 기본적으로 다른 방향으로 모나드를 볼 수 있는 방법이다.
- 이 Kleisli 범주론은 모나드를 편리하게 설명할 수 있는 기법으로 실질적인 문제를 해결하는데 사용할 수 있다.
물론 앞서 살펴본 4가지 예제가 전부는 아니다. 이 틀 안에서 표현할 수 있는, 전형적인 프로그래밍 모델에 대한 아이디어는 더 많고 다양하다. 이쯤해서 정리해두려 한다. 프로그래밍 언어와 모델에 대해 공부하고 분석하는 것에 관심있는 사람이라면 이미 범주론과 모나드와 같은 부분에 친숙할 것이다.
하지만 여전히 겸손한 프로그래머로서 새로운 언어를 디자인하지 않고, 프로그래밍 언어를 분석하는 논문을 작성할 일이 없으며, 단지 매일 마주하는 평범한 문제를 해결하기만을 원한다면? 이 질문도 정당하다. 모나드를 컴퓨터 프로그래머에게 함수가 의미하는 것을 이해하기 위한 수학적 형식주의로 남겨둔다면, 현역 컴퓨터 프로그래머는 모나드를 이해할 필요가 없다고 주장하기 좋을 것이다.
물론 이런 모나드를 실제 프로그래밍 문제에서 다루게 된다면 명백해지는 것은 분명하다. 과거에는 컴퓨터 프로그래머가 사용하는, 수정된 의미의 "함수" 즉, 앞서 살펴본 Kleisli 화살표는 프로그래밍 언어로서 만들어져 사용되었다. C에서의 함수는 Kleisli 화살표를 사용했다. 하지만 C++ 함수는 다른 것을 사용했다. 언어 명세는 그 언어의 함수를 사용하는데 어떤 것이 가능하고 어떤 것이 불가능한지 알려준다. 만약 그 명세와 다른 것을 원하고 있다면 그것은 좋지 않다. 아마 10년에 한번은 새로운 프로그래밍 언어로 갈아타고 그 언어의 새로운 기능에 일광욕을 해야 할지도 모른다.
과거: 오류 다루기
Err 모나드는 함수가 실패했을 때 실패에 대해 구조적인 방식으로 알려준다. 이 모나드를 모듈로 여기고 몇가지 정보와 확장을 넣는다면, 이건 기초적인 구조적 예외 처리가 된다. 역사를 살펴보면 프로그래머는 예외 처리 없는 언어로 오랜 기간동안 일해왔다. 물론 C와 같은 언어는 튜링 완전이며 연산 가능한 어떤 문제든 적절한 예외 처리와 함께 해결할 수 있었다. 하지만 가능한 연산을 생각하는데 범주론을 적용하진 않았다. 범주론은 합성을 어떻게 할 것인가에 대한 내용이다. C에서 제공된 예외처리와 같이 함수에 대한 개념이 없다면 프로그래머는 예외 처리를 위한 함수 합성을 수작업으로 해야만 할 것이다.
그 결과로, 어떤 C 함수든 실패를 하면 그 실패를 결괏값을 받는 것으로 확인한다. 많은 경우, 상투적인 지혜로 다음과 같이 말한다. "값을 반환하는 것으로 성공과 실패 여부를 확인하는 것이지 답을 받기 위한 것이 아니다." 함수 호출에 대한 코딩 컨벤션에서 호출에 대한 실패 여부를 확인하기 위해 if문을 사용해야만 하는데 그 결과 코드는 읽기 힘든 수준이 된다. 이것은 플로우차트(flowchart)와 의사 코드(pseudo code)의 전성기와도 같은데, 실제 코드에 대해 조금이라도 이해할 수 있을 것이라고는 기대하지 않았기 때문이다. 실제 세계에서는 물론 프로그래머가 가능하다고 생각했던 부분에 대해서만 오류가 확인 가능하고, 그 외에 수많은 오류는 감지되지도 않았다. 프로그램은 대개 신뢰할 수 없는 상태가 되었고, 말할 수 없을 정도로 막대한 비용을 추가적인 개발 작업과 문제 해결에 쏟았다.
이게 무슨 이유 때문일까? 그 이유는 단순하다. C 프로그래밍 언어와 그 시대의 다른 언어는 Kleisli 화살표와 같은 것을 불충분하게 제공했다! 만약 Kleisli 화살표가 함수적으로 지원하도록 언어에 포함되어 위에서 정의한 Err 모나드와 같은게 사용 가능했다면 피할 수 있는 문제였다. 하지만 C에서의 함수 관념은 고정되었고 그래서 반환값으로 그 문제를 처리해야 했으며 다른 언어로 변경할 수 밖에 없는 문제라서 수많은 프로그램이 다시 작성되었다. 이 과정에서 말할 수 없는 막대한 비용 또 들어갈 수 밖에 없었다.
현재: 전역 변수와 컨텍스트
_Pref_와 같은 모나드가 마음에 드는가? 앞서 언급한 것과 같이, 이 모나드는 커다란 문맥 내에서 정보와 상태의 세계에 접근 가능하게 한다.
과거에는 전역 변수를 사용했다. 좀 더 최근에는 이런 정보를 컴퓨터 메모리와 같은 알려진 장소에 저장한다. 쉽고 빠르게, 하지만 30년 전에도, 프로그래머는 전역변수라는 잘못된 답안을 가지고 있다는 것을 알고 있었고, 이는 대형 프로그램을 더욱 관리하기 어렵게 만들었다. 객체 지향 프로그래밍은 "객체"를 통해 특정 문맥을 함수를 구동하는데 활용해 이 객체 구현체를 암시적으로 전달하거나 최소 객체 자체를 구현하는 것으로 이 문제를 다소 해결하려고 했다. 이 문제를 해결하려면 모두 더 나은 Kleisli 화살표가 구현된 프로그래밍 언어로 변경해야 했다. 하지만 그건 쉽지 않은 일이다. 여전히 객체 지향 언어는 이 문제에 대한 완벽한 답을 주지 못하고 있다.
가까운 미래(와 현재): 순수성, 부수효과, 병렬성, 비결정론, 연속성 그리고 더!
이 부분은 미래에 대한 이야기지만 실제로는 이미 모두 가능한 것이다. 하지만 적절한 프로그래밍 언어를 선택하는 것이 필요하다!
컴퓨터 프로그래밍 커뮤니티에 있어서 현대적인 과제 중 하나는 효과적으로 병렬 프로그래밍을 제어하는 것이다. 아이러니하게도, 과거의 예제는 언어에서 Kleisli 화살표가 제공하는 문제 해결하는데 큰 힘을 보태지 못했는데 현대에는 그 힘이 너무나도 커졌다. 평범한 ("순수"라고 불려지는) 함수는 병렬처리에서 커다란 기회를 갖고 있다. 병렬로 코드를 실행할 때, 빠르게 실행되든 병렬처리 설계가 나뻐서 속도가 느리든 상관 없이 같은 결과를 제공한다. 하지만 Kleisli 화살표에 나쁜 업데이트가 제공되면 더이상 이 상황에 대한 해답이 되지 않는다. 이런 상태에서 병렬처리는 위험하기 때문에 예기치 않은 결과나 잘못된 결과 즉, 경쟁 상태 race conditions가 발생할 수 있다.
물론 Kleisli 화살표를 사용해서 파괴적인 갱신을 하는 코드를 마냥 제거할 순 없다. 관찰자 효과가 없는 프로그램은 유용하지 않기 때문이다. 어떤 점이 유용한가 보면 파괴적인 갱신을 수행하는 코드 중에 계산을 위한 순수함수를 분리하는 그 능력에 있다. 그래서 처음 시작할 때 둘 이상의 Kleisli 화살표를 지원하는 언어가 필요하다. 같은 언어에서 말이다!
이 소중한 기능을 최소 한 언어에서는 이미 지원하고 있다. 하스켈 언어 프로그래머는 모나드를 직접 만들 수 있고 그 모나드를 Kleisli 범주론으로 작성할 수 있다. 하스켈은 이 접근 방식을 가독성 높고 사용하기 편하도록 멋진 문법을 제공한다. 무언가 실패한다면 _Err_을 던질 수 있다. 만약 어플리케이션 설정에 접근해야 한다면, _Pref_를 던질 수 있다. 입출력 작업이 필요하다면 _IO_를 던질 수 있다. 하스켈 웹 어플리케이션 프레임워크나 이와 유사한 프로젝트는 모나드 접근 방식을 활용해 어플리케이션에서 요구하는 기능을 적절하게 구현하고 있다.
현재 컴퓨터 프로그래밍 커뮤니티의 또 다른 유행은 특정 도메인(domain-specific) 프로그래밍 모델을 만드는 것에 집중하고 있다는 점이다. Erlang은 병렬 처리에 대한 이점을 제공하는 새로운 프로그래밍 모델을 제공해 인기가 있다. Microsoft의 .Net 프레임워크에서 제공하는 LINQ는 대량 처리와 데이터 컬렉션을 다루는데 편리한 모델을 제공한다. Rails는 웹 어플리케이션을 위한 특정 도메인 언어로 유명해졌다. 그 외 언어에서도 더 유연한 방식으로 특정 연산을 더 쉽게 처리할 수 있는 방법을 지속적으로 제공하고 있다. 이 모든 예제가 새롭고 색다른 Kleisli 화살표를 이용해 주어진 일감에 알맞은 방식의 모델을 채택하고 있다.
위 이야기로 다음과 같은 결론에 닿는다. 만약 컴퓨터 프로그래밍에서 필요로 하는 모든 일을 적절하게 처리하는, 함수라는 단 하나의 개념에 도달했다고 생각한다면 실제 프로그래머로서 함수가 어떻게 정의되었는지 살펴보는 과정에 일반적인 개념의 모나드와 Kleisli 화살표를 유물 이론가처럼 알아야 할 필요가 없어진다. 하지만 알 필요 없다는 식의 접근법으로 생각하진 말자. 프로그래밍 커뮤니티는 빠르게 변동하고 있다. 다른 맥락, 다른 일감, 물론 다른 개별 어플리케이션을 위한 함수의 정의는 각각 다른 개념을 향하고 있다. 그래서 언어와 도구, 그리고 다른 절차상의 추상성을 비교하는 통찰을 갖는 것은 유용하다. 이것이 모나드가 우리에게 주는 교훈이다.
모나드를 뛰어 넘는 추상
모나드를 제공하는 언어를 선택해 사용하는 것에는 다른 이점도 있다. 바로 추상을 제공한다는 점이다. 예를 들어, 하스켈의 경우, 하나의 코드를 작성해 여러 종류의 다른 모나드를 사용하는 접근이 가능하다. 하나의 모나드를 위해서 작성해야 하는 엄청나게 많은 양의 코드를 작성해야 한다면 사실 아주 다른 의미의 모나드가 될 것이다. 예를 들면 다음 하스켈 타입을 고려하자:
sequence :: Monda m => [m a] -> m [a]
이 코드에서 어떤 모나드든 _M_이라고 부를 수 있다. sequence는 값 목록 _M(A)_를 _M(List(A))_로 넣는데 모나드는 리스트 그 자체에 적용된다. 앞서 작성한 4가지 예제에서 이 코드가 어떤 의미를 갖는지 시간을 두고 생각해보자. _Err_에서는 실패할 가능성이 있는 목록의 결과를 넣는다. 만약 그 목록에 실패하게 되는 값이 있다면 실패할 것이다. 실패하지 않는다면 값의 목록을 반환할 것이다. 이는 기본적으로 모든 실패를 판단해야 할 전체 목록의 연산에서 편리한 방법이 된다. _Pref_에서는 어플리케이션 설정을 담은 단일 집합을 받는데 목록으로 제공되는 어떤 것이든 결과 목록을 반환된다. 멱집합 모나드 _P_에서는 집합 목록을 받아 각각의 집합에서 요소를 선택하는 모든 방법을 반환한다. _IO_에 대해서는 지시에 대한 목록을 받아 그 지시를 하나씩 실행해 모두 처리한 후 반환한다. 놀랍게도, 이 하나의 함수는, 단 한 번의 구현으로 이 4가지 모나드 예시에 적용할 수 있고 유용하게 활용할 수 있다!
모나드를 아는 것은 그것으로 끝나는게 아닌, 그 이상의 추상적인 능력을 얻게 되는 것이다. 이 능력은 어떤 모나드를 작성할 상황이 오든 납득 가능하고 의미있는 코드를 작성하는데 도움이 된다.
다음 10년은, 지금 수많은 개발자가 디자인 패턴이나 에자일 기법에 대한 용어를 사용하는 것과 같이 모나드라는 강력한 무기에 대해 이야기하게 될 것이란걸 예견한다.
모나드를 넘어: 범주론적 프로그래밍 더하기
모나드에 대해 얘기하는 동안, 컴퓨터 프로그래밍에서 범주론이 주는 영향에 인상을 받은 사람이라면 그냥 떠나지 않길 원한다. 다음과 같은 모든 아이디어는 실제 프로그래밍에서 찾을 수 있다. 물론 대부분 (지금까지는) 유연하고 깊은 학문적 문화와 역사를 가진 하스켈 프로그래밍 언어 커뮤니티에서 말이다.
- 모나드 변환기 Monad transformers는 풍부하고 강력한 프로그래밍 모델을 만드는 하나 이상의 모나드 효과를 결합하는 강력한 기법이다.
- 함자 Functors와 적용가능 함자 Applicative functors (수학에서 강한 lax monoidal functors라고 부르는) 둘은 모나드보다 약하지만 더 넓은 범위에 적용할 수 있다.
- 때로 Kleisli 범주 외에 다른 범주도 정의해 특정 문제를 차분하게 해결하기 위해 사용할 수 있다. Freyd 범주도 유용하다.
이 글을 여기까지 다룰 예정이지만, 범주론에서 배울 수 있는 다양한 추상을 살펴보는 것은 아주 유용하기에 살펴보길 바란다. (하스켈 특정이지만) Brent Yorgey의 Typeclassopedia도 좋은 시작점이다. 범주 기반의 개념, 통찰을 컴퓨터 프로그래밍에 적용할 수많은 가능성은 단지 문만 열면 만날 수 있을 것이다.
여기서 다룬 개념을 설명한 것은 내가 다 만들어낸 것이면 좋겠지만, 실제로는 수년간 컴퓨터 프로그래머가 해온 일들로 만들어진, 자연적인 확장에 가까운 것이다.