비수학인이 해석한 모노이드 - 모노이드가 왜 중요할까?

Posted on February 16, 2023

비수학인이 해석한 모노이달 카테고리 글과 일부 중복되는 내용이 있습니다.

“왜 마그마magma, 반군semigroup, 모노이드monoid, 군group 등 많은 추상 대수 구조가 있는데, 이 들 중 모노이드가 왜 특별한 대우를 받고 있을까?”

“항등원이, 아무 일도 안하는 구성원이 왜 필요할까?”

위 두 가지 질문에 대한 답을 아시거나, 위 질문이 궁금하지 않은 분들에겐 적당한 글이 아닙니다.

완벽한 학문적 정의를 풀어 쓰려는 글이 아닙니다. 비수학인인 프로그래머로서 관심을 가지는, 모노이드 속성에 관한 얘기입니다. 믿을만한 텍스트를 읽고 번역하거나 정리한 글이 아니니, 매우 얼기 설기할 것으로 예상합니다. “이렇게 이해, 혹은 오해를 하는 사람도 있구나” 정도로 읽어주세요. 오해로 판단된다면 꼭 댓글 부탁드립니다. 지금까지 아래처럼 설명한 자료를 본 적이 없으니, 틀릴 확률이 높습니다. 정확하지도 않은 글을 올리는 이유는, “정확하게 알고”싶어서 입니다.

“한 가지 모양으로 보자”

제가 생각하는 모노이드는 바로 위 문장에 관한 얘기입니다.

마술사가 보여 준 카드는 한 장이 아닐지도 모릅니다

용어

Product

정확한 수학 뜻은 다른 자료를 참고해 주시고, 저는 두 가지 정보를 하나로 합치거나, 적어도 하나의 이름으로 부를 수 있게 해주는 연산으로 보고 있습니다. 순서쌍(2튜플) (a, b)ab를 합쳐 튜플로 부를 수 있습니다. 하나로 합쳤지만, 개별 요소에 구별해서 접근이 가능합니다.@todo

Product 카테고리

Monads in Haskell and Category Theory - Samuel Grahn

카테고리 두 개 C, D를 product하면 카테고리로 C × D로 표시합니다. 카테고리를 product한다 해서 카테고리 내부를 안 보나 했는데, Set 두 개를 product할 때 처럼, 안에 있는 오브젝트들을 가지고 와 2튜플을 만듭니다. 2튜플이란 걸 처음 것에 접근, 두 번째 것에 접근 가능한 무언가로 정의하며 카테고리식으로 표현하니, 결국 오브젝트를 직접 보진 않습니다. 단, 카테고리는 모피즘이 같이 존재하니, 모피즘도 가지고와 따로 2튜플을 만듭니다. 오브젝트들은 (a, b)로, 모피즘은 (f, g), 합성은 (f, g) ◦ (α, β) = (f ◦ α, g ◦ β) 이런 식으로 항상 두 가지 정보가 존재한다를 유심히 봤습니다.

모노이달 카테고리

아래보다는 비수학인이 해석한 모노이달 카테고리 글을 참고해 주세요.

대수학 모노이드(아래 수학에서 모노이드 참고)와 비슷한데, 전 역시나 하나의 모양으로 보는 것으로 생각했습니다. 카테고리 하나로 봐도 좋고, 전부 가 들어간 것으로 보는 걸로 봐도 좋습니다. 어차피 가 들어간 것도 결국 C 하나가 되니 어떻게 봐도 상관없습니다. 같은 모양으로 본다는 건 같습니다.

추상 대수학에서 별도 법칙으로 두었던 항등원과 결합 법칙을 함수(Natural isomorphism)로 표현합니다.
이항 함수에, Left unitor λ(람다), Right unitor ρ(로), Associator α(알파) 함수들이 있어 모노이드 구조가 되게끔 해주는 차이가 있습니다.

카테고리 이론에서 자연수, 더하기 모노이드

0포함 자연수의 더하기 모노이드를 보자면, 0,1,2,...들을 모두 모피즘으로 놓고, 오브젝트는 하나만 있으면 됩니다. 이 오브젝트에서 출발해서 이 오브젝트로 도착하는 모피즘, 즉 엔도 모피즘들에 0,1,2,...를 붙이면 됩니다. 그리고, 모노이드의 이항 함수는 이 모피즘들의 합성으로 표현됩니다. 모피즘이 함수가 아닌 예라고 합니다. 합성compose할 때 더하기 성질과 같도록 룰을 잘 정하면 됩니다. 11을 compose하면 2 모피즘이 나옵니다.

언뜻 이해가 안갑니다만, 어차피 이항 함수 몇 번을 적용해도 처음과 같은 모양이 된다면, 오브젝트는 하나라고 예상할 수 있습니다.

모나드

위 모노이달 카테고리의 오브젝트가 엔도펑터 T, M 이고(무수히 많겠지만 간단히 말하기 위해 두 개만 있다 보면), 이항 연산에 해당하는에 타입 레벨 합성Type level compose 를 넣으면, T ◦ M, M ◦ T, T ◦ T, M ◦ M이 결과로 나옵니다. 만일, 이 중 T ◦ T를 고른다면, (TMaybe라고 하면 Maybe ◦ Maybe를 꺼내온 상태입니다.) 이 때,
join(μ) :: T ◦ T -> T 함수가 있고,
return(η) :: 1 -> T 함수가 있어(1 연산에 대한 항등원),
join이 이항 함수, return이 항등원 역할을 하고(1을 변환할 때 씁니다.),
join이 결합 법칙을 따라서
T,join,return이 모노이드가 되면 모나드라고 부릅니다.

결합법칙

join이 결합 법칙을 따르는지 보려면, join이 이항 함수가 아니라서 잠깐 멈칫 합니다. 이항으로 받지 않고, 마치 2튜플로 받는다고 예를 들어 생각하면 편합니다. (실제는 이항 연산의 일반화에 대한 이해가 필요할 듯 합니다.)만일 이항이었으면 T join (T join T) = (T join T) join T 였을테고, 이를 튜플로 받는다 생각하면,
(보통은 다이아그램으로 그려져 있고, 저도 그림을 더 선호하는데, 이 경우엔 코드가 더 명확하게 보입니다.)

join (T ◦ (join TT)) = join ((join TT) ◦ T)
           join (TT) = join (TT)
                      T = T

항등원

join에 대한 항등원을 보려면,

join (1T) = join (T1) = T

이 걸 봐야 하는데, joinT가 두 개 있어야만 적용 가능합니다. 1 ◦ TT가 하나라 적용을 못합니다. 그럼 이 때 T를 하나 더 넣어주는 return을 넣는 트릭으로 풀면 됩니다. 전 애초에 기본형?이 T가 아니라 T ◦ T이라 생각하고 있습니다.

join (return 1T) = join (Treturn 1) = T
       join (TT) = join (TT) = T
                  T = T = T

1만으로 항등원이 되는게 아니라 return 1이 항등원 역할을 한다고 말하면 편한데, 이렇게 얘기하는 것도 맞지 않습니다. 이항 연산에 해당하는 걸 \x y -> join (x ◦ y)라 하면 return 1이 항등원 역할이지만, join일 때는 이렇게 얘기할 수 없습니다.

※ 위 바이펑터에 텐서 프로덕트를 넣어 주는데, Compostion을 넣어 준다 하여, 보통의 함수를 합성compose하는 (.)을 넣어 주는 줄 알고 생각이 꼬였습니다. 엔도펑터 a -> m aa -> m a를 보면, 입출력 타입이 달라 합성을 할 수 없습니다. 바이펑터 자리엔 (.)이 아니라 타입 레벨 합성이 들어가는 걸로 봐야 합니다.(@다믜님이 알려 주셨습니다. 감사합니다.)

타입 레벨 합성은 다음을 말합니다.

펑터 a -> Maybe a, a -> [a]
두 개가 있을 때 값 레벨에선 입출력이 달라 합성이 안됩니다. (.)로 뭘 어떻게 할 수 가 없습니다.
타입 레벨에선 [Maybe a] 또는 Maybe [a] 합성이 되는 걸로 보면 됩니다.
타입 레벨에서 두 타입을 합성하면 새로운 타입이 생깁니다.즉 Maybe[]를 합쳐 “새로운” 타입 Maybe [] 또는 [Maybe] 타입이 생깁니다. 하스켈에선 다음 클래스가 이 역할을 합니다.

Data-Functor-Compose

쉽게 얘기하면, Maybe 펑터를 두 번 적용하면 Maybe (Maybe _)가 됩니다.

모노이드monoid 단어

어미에 ~oid가 붙으면, “딱 그 건 아닌데, 그 것 같은 것들”을 뜻합니다. humanoid, android, roboid 주로 인간을 흉내 내는 인간 같은 로봇에 붙는 말 등으로 만났던 말입니다. 뭔가 mono는 아닌데, mono같은, mono로 볼 수 있는 무언간가 봅니다.

수학에서 모노이드

어떤 시스템에 이항 연산이 있고, 항등원이 존재하고, 이 이항 연산이 결합 법칙을 만족하고 닫혀 있으면 어떤 것이든 모노이드라 부릅니다.

(※ 이 정의만으론, 어떻게 함수형 프로그래밍과 연결되는지 알게되는데까지 정말 많은 시간과 노력이 필요합니다.)

수학 모노이드 설명을 내가 필요한대로 해석

말을 잘 못하면 완벽한 해석을 주장하는 것으로 오해 받을 수 있어, 다음 말부터 조심스럽게 강조하고 이어 가겠습니다.

“지금 여기서 얘기하는 것이 모노이드의 모든 특징”

이란 말을 하고 싶은 게 아닙니다. 혹은, 이렇게 해석해야 한다는 주장도 아닙니다. 딱, 제가 프로그래밍할 때 필요한 정보만 꺼내보면, 혹은 프로그래밍 관점에서 모노이드의 정의를 해석하면 아래와 같은 특징을 잡아 낼 수 있다는 걸 말하는 중입니다.

“어떤 시스템이 있고, 이항 연산과 항등원이 존재해서 이 시스템에 있는 구성 요소는 모두 이 연산이 쓰인 한 가지 모양으로 표현할 수 있다”

이 말을, 사례를 들어 풀어 보면 다음과 같습니다.

자연수, 더하기 모노이드

0이 없는 자연수 시스템이 있습니다. 1,2,3...
그리고, 더하기 (+) 이항 연산이 있습니다.
211을 더한 것으로 표현할 수 있습니다. 2 = 1 + 1
312를, 혹은 21을 더한 것으로 표현할 수 있습니다. 3 = 1 + 2

그럼 1을 더하기를 쓴 모양으로 표현하려면 어떻게 할까요?

지금 상황에선 표현할 방법이 없습니다.

여기에, 0을 추가해 보겠습니다. 1이나 2,3,4... 어떤 구성 요소에 더하기 0을 하면, 원래 숫자가 그대로 나옵니다. “아무 일도 일어나지 않습니다.” (추가적인 조건을 엄밀하게 만족하면 항등원Identity이라 부릅니다만, 섣부른 정의는 오해를 살 수 있으니, 정교한 정의는 추상 대수학등을 참고 해주시고, 여기서는 필요한 뜻만 보겠습니다.) 보통은 이런 설명만 강조하고 추가 설명은 없는 경우가 많습니다. 이 0을 이용하면 비로소 1도, 지금 새롭게 추가하는 0도 더하기를 쓴 모양으로 표현할 수 있게 됩니다.

101을 더한 것으로 표현할 수 있습니다. 1 = 1 + 0
000을 더한 것으로 표현할 수 있습니다. 0 = 0 + 0

시스템에 0을 추가하면서, 0을 포함한 모든 구성 요소를 a + a 꼴로 표현할 수 있게 되었습니다. 0이 있어, 모든 구성 요소를 하나의 모양으로 추상화할 수 있게 되었습니다. 이 “추상화” 말이 불편한 분은, 모두 같은 모양으로 표현할 수 있게 됐다로 넘어가도 좋습니다.

모두 같은 모양으로 볼 수 있다는 건 대단한 특징입니다. 이 말을 타입으로 확장해 보겠습니다.

반군semigroup과 차이

반군

모노이드에서 항등원을 빼면 반군이 됩니다. 반군도 역시 이항 연산은 닫혀 있기에, 어떤 구성원 둘의 연산 결과도 다시 반군의 구성원이 됩니다. 자연수 1,2,3,4,5...있을 때 어떤 두 구성원을 골라도, 결과도 자연수가 됩니다.

항등원

여기에 항등원이 있으면 무슨 일이 생길까요?

“자기 자신도 연산이 들어간 모양으로 표현할 수 있습니다.”

아무 일도 일어나지 않는 항등원으로 자신을 표현하는 게 왜 필요할까요? 연산은 “변화”를 의미하는데, 이를 무력화 시킬 수 있는 항등원이 있으면, 프로그래밍에서 더 유용해지는 이유가 뭘까요?

11 + 0의 차이는 무엇일까요?

타입

Maybe타입을 정의하고 값 생성자 Just 혹은 Nothing으로 타입을 만들어 냅니다. 이렇게 만들어진 Maybe aa에 접근하려면 반드시 어떤 절차를 거쳐야 합니다. 타입은 “추가 절차”를 가지고 있습니다. 생성자도 하나의 계산식computation입니다. 위의 연산 +를 타입 안에 실어(넣어) 보겠습니다.

data AddType a = AddType a a

이 타입의 안을 볼 때는 +라는 절차를 하겠다고 약속합니다. 안에 있는 값에 접근할 때는 언젠가는 이 절차를 거친 후 접근 할 겁니다. (다르게 얘기하면, 이 타입의 안을 본다는 건, 어떤 함수를 적용하려고 할 때를 말하는 것이고, a 타입에서 쓰던 함수를 AddType a에 적용하는 법을 보통 fmap으로 정의하고, 펑터라 부릅니다.)

1 + 2AddType 1 2로 표현할 수 있습니다. 1 + 2를 한 것에 3 + 4를 더한다면, AddType 1 2AddType 3 4AddType에 넘겨야 합니다. 계속 AddType로 생성하는데 문제가 없게 하려면 AddType는 인자로 AddType를 받아야 합니다.

자연수 전부를 AddType로 보려합니다. 반군이라면, 다른 건 어찌 다 표현한다 처도 1AddType로 표현할 방법이 없습니다. 1 + 1에 다시 1을 더해 3을 표현하려면

AddType (AddType 1 1) 1 -- type error 

타입이 달라 할 수가 없습니다. 항등원이 있어야 비로소

AddType (AddType 1 1) (AddType 1 0) -- ok

으로 표현할 수 있습니다.

“항등원이 있으면 구성원을 모두 하나의 타입으로 표현할 수 있습니다.”

data AddType a = AddType (AddType a) (AddType a) | Zero -- ok

a -> a , (.) 모노이드

※ 이하 (.)는 함수 합성이 아니라, 타입 레벨 합성으로 보겠습니다.

구성 요소가 자연수같은 값이 아닌, 위 AddType처럼 계산식compuation인 a -> a 형태의 함수일 경우를 보겠습니다. 짧게 쓰기 위해 a -> aFunc 모양이라 부르겠습니다.

type Func = a -> a

f, g, h ... :: Func 라는 구성 요소들이 존재하고, 이들 사이의 관계를 표현하는 이항 연산 (.)이 있습니다. 이 연산은 먼저 들어온 함수를 적용현 결과를 다음 함수의 입력으로 넣어주는 함수입니다. (.)을 타입 안으로 넣겠습니다.

data CompType x y = CompType x y

위 더하기와 비교하며 보시기 바랍니다. 모든 Func 함수는, Func들을 (.)로 엮은 걸로 표현할 수 있게 하고 싶습니다. (왜 이렇게 하고 싶냐는 결론에서 보겠습니다.) 어찌 됐든 지금은 한 가지 모양으로 표현 된다는 것만 보시기 바랍니다.

CompType (CompType f1 f2) (CompType f3 f4)

여기서도 더 이상 (.)을 쓴 모양으로 쪼개지지 않는 함수가 있다고 보겠습니다. 이런 경우에도 만일 “아무 것도 안함”이란 요소가 존재한다면, 위 자연수 경우처럼 Func가 구성 요소인 시스템의 모든 구성 요소를 (.) 모양으로 표현할 수 있습니다. 이 경우 자연수의 항등원과 같은 역할을, id함수가 합니다.

Func = Func . id

a -> m a 가 구성 요소인 모노이드

구성 요소가 a -> a 형태가 아니라, a -> m a 형태의 구성 요소들이 있고 (.)이 있는 경우를 보겠습니다. 표현하기 편하게

type M = a -> m a  

M이라 부르겠습니다.

M . M -> M

즉, 모든 구성원을 연산을 앞에 두는 전위 표현식으로 쓰면 (.) M M 모양으로 보는 게 목표입니다. 이 모양을 ActionType 타입이라고 하겠습니다.

data ActionType x y = ActionType x y

처음 M을 적용하면 결과는 m a가 되고, 이를 두 번째 M의 입력으로 넣어 주면 결과는 m (m a)가 됩니다. 즉 M :: a -> m aM :: a -> m a(.)로 연산하니, 합성의 결과는 a -> m (m a)가 돼버렸습니다.

(.) M M :: a -> m (m a) -- 결과가 M :: a -> m a 모양이 아닙니다.

시스템의 모든 M(.)을 쓴 한 가지 모양으로 표현이 불가능한 상황입니다.

join

그런데, 여기에 join :: m (m a) -> m a란 함수가 있다면 어떻게 될까요? (.)을 적용 후 연이어 join을 적용하면 a -> m a가 되니, 구성 요소들을 모두 (.) 적용 후 join로 표현할 수 있을 것처럼 보입니다.

M =타입같음= M `(.) 적용 후 join` M
-- 양쪽이 완전히 같은 대상이란 뜻이 아니라 "타입이 같다"는 뜻으로 썼습니다.

하지만, 아직 해결되지 않은 한 가지가 남아 있습니다.

return

M =타입같음= M `(.) 적용 후 join` id  

이 되어야 하는데, a -> m aid(.)을 적용하면, 이 번엔 결과가 그 대로 a -> m a 이어서 join을 적용할 수 없습니다. 그래서, return :: a -> m a를 정의해서 id의 경우에는

M =타입같음= M `(.) 적용 후 join` (return id)

이 되게 하면, 비로소 id를 포함한 모든 구성 요소를 한 가지 (.) 적용 후 join 모양으로 표현할 수 있게 되었습니다.

정리하면, joinreturn이 있어 M, (.)이 모노이드로 기능하게 만들면, ActionType라는 타입으로 구성원을 모두 표현할 수 있게 됩니다.

생각 스트레칭 - 대상을 변환할 것인가, 연산을 변환할 것인가

2튜플 둘을 비교하는 함수가 있습니다.

type Height = Int
type Age = Int

equal :: (Height, Age) -> (Height, Age) -> Bool
equal x y = x == y

그런데, 현재는 키Height만 같으면 같은 것으로 보려 합니다.

equalH :: (Height, Age) -> (Height, Age) -> Bool
equalH x y = fst(x) == fst(y)

이 번엔 나이만 같으면 같은 것으로 보려 합니다.

equalA :: (Height, Age) -> (Height, Age) -> Bool
equalA x y = snd(x) == snd(y)

둘이 유사하니, 같은지 비교하기 전에, 필요한 부분만 남기는 함수를 넘겨 아래와 같이 합칠 수 있습니다.

type F = (Height, Age) -> Int
equal :: F -> (Height, Age) -> (Height, Age) -> Bool
equal f x y = f(x) == f(y)

이제 equal을 부를 때, equal fst t1 t2 혹은 equal snd t1 t2로 부르면 됩니다.

그럼, 튜플 전체를 비교하던, 원래 f를 받기 전 equal의 동작을 하게 하려면 어떻게 하면 될까요?

equal id t1 t2로 부르면 됩니다.

하스켈은 커링이 되니 다음처럼 얘기할 수도 있습니다.

equal을 부르지 않고, equal fst, equal snd, equal id 등으로 “변환”된 비교 함수로 쓰면 됩니다.

튜플 둘을 상황에 따라 비교하기 위해, 각 튜플을 변환하던 작업을, equal로 옮겼습니다. 값 자체를 변환하지 않고, 값을 비교하던 동작을 변환했다고 말 합니다.

모나드는 모노이드의 일반화일까?

눈치 채셨겠지만, 위 M은 알고 계신 바와 같이 바로 모나드입니다. 엔도펑터가 오브젝트인 모노이달 카테고리에서 return 의 도움을 받아 M (.) Mjoin연산으로 모노이드가 되는 걸 모나드라 합니다. 모노이드는 이항 연산을 받는데 join은 단항 연산을 받으니 처음엔, 이 걸 연산자로 보는 게 아닌가 했습니다. 모노이드 이항 연산의 동작을 보면 두 개를 받아 하나로 줄이는 작업입니다. join :: m (m a) -> m a의 동작도 역시 두 개를 하나로 줄이고 있습니다. 하지만, 이미 m (m a) 으로 만들어진 걸 받아야 합니다.

지금부턴 상상입니다.

m (m a)m ⨂ m에서 부분에 를 넣어주고 생략한 모양일 뿐입니다.  모노이드의 이항 연산의 동작을 분해해 보면, m 두 개를 받고, 이를 합치는 절차를 거친 후, 하나로 만드는 작업으로 볼 수 있습니다. 모노이달 카테고리에선 카테고리에 있는 오브젝트 두 개를 가져와 일단 카테시안 카테고리를 만드는 절차가 나옵니다. 그 후 카테시안곱 형태로 되어 있는 오브젝트 (a,b) 하나를 바이펑터에 넘깁니다. 모나드는 특별히 엔도펑터를 대상으로 하고, 바이펑터로 “타입 레벨 합성”을 쓴다는 상황에서 나오는 구조입니다.

어쨌든, 이와 유사하게 product를 합치는 작업, 그 걸 다시 하나로 만드는 작업으로 나눈다면, join은 하나로 만드는 작업에 해당합니다. 모노이드 이항연산이었다면 (1 + 2) + 3 ... 이런식으로 체인되는 모양이 나올 수 있지만, joinjoin (join (join m ⨂ m)) 같이 할 수 없습니다. 위 join의 결합법칙을 잘 보시면 join (T ◦ (join T ◦ T))으로 join이외에 바이펑터를 같이 쓰고 있습니다.

원래 하나의 동작(모노이드 이항 연산)으로 봤던 걸, 두 개의 절차로 나눴습니다. 기존 하나로 된 동작은 이 것의 스페셜한 동작 중 하나로 볼 수 있으니 모노이드 이항 연산 보다는 모나드의 , join이 더 일반적으로 볼 수 있습니다.

그동안 봐왔던 모노이드를 바로 이 모나드로 표현할 수 있습니다. 절차가 나뉘어져 있지만, 첫 번째 절차로 원래 쓰던 이항 함수를 쓰고, 두 번째 joinid로 구현하면 기존 모노이드와 다를 바 없습니다.

예를 들면, Func :: a -> a합성(.) 모노이드는 returnjoinid로 놓은 것으로 볼 수 있습니다. 그리고 카테고리이니 모피즘 합성도 있어야 하는데, 이 부분이 없는 걸로 보면, 기존 추상 대수의 모노이드가 됩니다. 이렇게 보면 일반화로 볼 수 있습니다.검증 필요

왜, 한 가지 모양으로 보는 게 중요할까?

금방 눈에 보이는 건, “연이은 Composition이 가능하다” 인데, 이 걸 조금 풀면 다음과 같이 말할 수도 있습니다.

함수형은 함수가 함수를 감싸는 모양이 계속 되는데, 만일 m a를 입력으로 받아야 하는 함수가 있고 모나드라면, 여기에는 m a도, m (m a)도, m (m (m ...도 넘길 수 있게 됩니다. 이 게 가능하게 되면 함수 합성을 덩어리, 덩어리로 쪼개어 모듈화가 가능해집니다.

하스켈로 얘기하면, Maybe 타입이 있는데, job1의 결과도 Maybe이고, job2의 결과도 Maybe이면 이 두 job(.), join, return 으로 합성해서, 타입이 변하지 않은 Maybe타입의 job을 만들어 낼 수 있다는 말입니다.

어떤 함수가 Action을 받는다면 이 함수에는 (join혹은 return을 변환이라 표현했습니다.)

Action1 (변환 연산) Action2

를 넘겨 줄 수도,

Action1 (변환 연산) Action2 (변환 연산) Action3 (변환 연산) Action4 (변환 연산) Action5

를 넣어 줄 수도 있게 됩니다. 결합 법칙을 만족하니, Action 1,2를 먼저 묶고, 3,4,5를 묶는다든지, 1,2,3,4를 묶고, 5를 묶는다든지 할 수도 있습니다. 함수형 프로그래밍의 구조 자체에 모노이드 개념이 깊이 들어가 있습니다.

(※ 여기서 얘기하는 모노이드, 모나드는 하스켈의 모노이드 클래스, 모나드 클래스를 말하는 게 아닙니다. 모노이드 “개념”을 함수형과 어떻게 붙이는가에 대한 얘기입니다. )

함수형 프로그래밍의 근간이 되는 중요한 특징 중 하나로 볼 수 있습니다.

모노이드는 모든 값을 계산식으로 볼 수 있습니다.

상상 혹은 잡소리

보통은 모노이드 예시를 들 때, 자연수내에서 임의의 두 수를 골라 연산을 해도 자연수가 나온다고 설명합니다. 하지만, 이 건 반군도 가지고 있는 성질입니다. 이 글은, 여기에 연산을 밀어 넣어 살짝 비틀어본 상상입니다. (연산을 여러번 한 것이 연산 한 번 한 것과 같은 것을 보이기 위해서 떠올린 아이디어입니다.)

0을 포함한 자연수 집합의 구성원들을 더하기란 연산에 모두 의존하는 것으로 바라 보면, 하나의 구성원은 다른 구성원들과 더하기로 어떤 관계가 되는가로 나타낼 수 있습니다. 모든 구성원들은 항등원과 관계를 맺고, 다른 구성원들과 더하기 관계로 모두 나타낼 수 있습니다. 32+1의 관계가 있고, 1+2의 관계가 있습니다. 21+1의 관계가 있습니다. 다른 것들과의 관계로 구성원을 표현할 수 있다고 볼 수 있습니다. 이렇게 모든 구성원을 표현할 수 있는 원자적인 요소로 0, 1, +가 존재하는 것처럼 볼 수도 있습니다. 재귀 함수가 종료 조건을 갖듯이, 반복되는 관계로 표현할 때 항등원이 종료 조건이 되는 것으로 볼 수도 있습니다.

하스켈에서 모노이드, 모나드를 공부하며 자꾸 끌리는 개념이 바로 ’같음’입니다. Maybe를 예를 들면, 결과를 모두 Maybe로 만들어 같음을 만들어 간다고 볼 수도 있고, 결과를 모두 Maybe 연산 Maybe로 만들어 같음을 만들어 간다고 볼 수도 있습니다. 전 같음에 중요한 역할을 하는 연산을 끌여 들였습니다. Maybe 연산 idMaybe로 표기할 뿐입니다. 이 같음을 프로그래밍에서 유용하게 쓸 수 있도록 해주는 깔끔한 개념이 모노이드인 것으로 보고 있습니다.

※ 여기서 제가 얘기한 ’같음’과 같은 지는 모르겠는데, ’같음’을 연구하는 학문이 Homotopy Type Theory라고 합니다. 매우 어렵다니, 비수학인이 눈길을 줄만한 건 아닌 것 같습니다.

사족

항등원이 추상화(혹은 일반화) 도구로 유용하게 쓰일 수 있다라고 말하니, 여러 분들이 오개념이라 지적해 주셨는데, 그 내용이 일부 들어가 있습니다. 아직도 어느 부분이 오해를 하고 있는지 잘 모릅니다. 어차피, 제 블로그를 보는 분은 극 소수이긴 하나, 다시 한 번 위험성을 강조하겠습니다. 저처럼 추상 대수의 모노이드 설명만으론 궁금함이 해결되지 않는 분이, 어쩌다가 이 글을 본다면 같이 “상상하고 고민한다”는 생각으로 읽어 주시기 바랍니다. 정답을 전달하는 글이 아닙니다.

위는 엄밀하지 못하게 설명한 부분 투성입니다. 위와 같은 관점으로 해석하는 글을 본 적이 없는 이유일지도 모릅니다. “틀렸을지도 모른다”는 주의문을 달고 자유롭게 글을 쓰는 블로그 수준의 글임을 감안해 주시기 바랍니다.

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