한 번쯤 나올 법 했는데, 아직 없었던 펑터 이야기

Posted on March 9, 2023

학술적인 내용이 아니라, 펑터 이해를 위해, 이제 막 출발한 사람이 펑터를 소개하는 정도의 글입니다. 그러니, 당연히 정답 목적지에 도착하지 않은 글입니다. 잘못된 정보를 올려 놓은 게 보이면 꼭 지적 부탁드립니다.

처음 접하길 펑크터란 발음으로 접해, 다른 분들과 대화를 나누기 전에는 펑크터로 n년동안 발음했는데, 국내 업계는 펑터란 발음으로 자리 잡은 것 같습니다. 이 블로그의 초기 글엔 펑크터로 써있고, 2023년 이 후 글들은 펑터란 말로 바꾸기 시작했습니다.
※ 원래 발음 기호는 fəŋ(k)-tər이므로 k를 넣어도, 안 넣어도 맞는 발음입니다.

(번역어로 함자란 말이 있지만, 저와 대화를 나누는 대다수의 분들이 펑터를 더 편하게 생각합니다.)

모노이드, 모나드 관련 블로그 포스트를 몇 개씩 쓰는 동안, 정작 먼저 알고 있어야 하는 펑터 글은 하나도 쓰지 않았습니다. 다른 분들도 쓰윽 무리없이 지나간 키워드, 개념일 수도 있는데, 조금 더 성질을 알면 모노이드, 모나드를 보는데도 도움이되고, 나아가 함수형 설계에도 도움이 됩니다. 보통 펑터를 설명하는 텍스트를 만나면 여기와 같은 얘기를 하지 않습니다. 독특하게 쓸모가 있거나, 전혀 읽을 가치가 없는 글일 수도 있습니다.

  1. 아주 간단하게 보는 카테고리 이론에서 펑터
  2. 똑같은 대상과 닮은 대상
  3. 컨테이너란 선입견에서 벗어나자
  4. fmap
  5. endofunctor
  6. 그 다음은?

보통은 “컨테이너가 있고, 그 안에 든 대상에 매핑할 수 있는 방법을 가진 타입”을 펑터라고 설명하곤 합니다. 여기선, 펑터와 관련된 어려운 수학을 아주 아주 제멋대로 비수학적, 인문학적으로 해석합니다. 위 목차대로 짚어가며 제가 이해하고 있는 펑터 이야기를 해보겠습니다.

간단하게 보는 카테고리 이론에서 펑터

함수function도 매핑 동작을 하고, 펑터functor도 매핑 동작을 합니다. 어차피 매핑 동작을 하는 거면, 같은 이름으로 부르지, 왜 함수를 놔두고 다른 이름 펑터를 만들어냈을까요?

구조

어떤 시스템에서 구성원들이 다른 구성원과 어떤 관계들을 갖느냐를 “구조”라고 합니다.

카테고리

집합은 구성원들 몇 개가 모인 것이고, 이 집합에 규칙을 적당히 두어 마그마, 반군, 모노이드, 군같은 구조로 부릅니다. 반면, 카테고리란 구성원이 대상만 존재하는 것이 아닌 대상object, 대상들간의 모피즘morphism, 모피즘 합성composition, 항등모피즘Identity, 결합 법칙이 있는 “구조”입니다. (대상이 있고, 그 들 사이에 있을 수 있는 관계의 가장 일반적인 표현이 모피즘인데, 하스켈로 좁혀서 보면 모피즘들은 대부분 함수 형태로 나옵니다.) 왜, 이런 구조를 만들었는지는, 이론을 공부해 나가면서 점점 알게될텐데, 입구에서 몇 걸음 가지 않은 저로서는, “구조 보존”을 표현하기 좋게 모아놓은 요소들 아닌가 정도 추측하고 있습니다. 제 지식으로 몇 줄로 요약한다는 건 불가능에 가깝지만, 펑터를 얘기하려면 할 수 없이 짚고 가긴 해야 합니다.

펑터

카테고리 이론은 어떤 구조를 가진 한 카테고리를, 다른 카테고리로 매핑하면서 여러가지 수학적인 개념을 표현합니다. 대상의 수학적 성질을 표현할 때, 대상 자체를 언급하는게 아니라 오로지 다른 대상과의 관계로만 설명합니다. 실제 느낀 효과로는, 한 카테고리에서 표현하기 난해한 것들을 다른 카테고리에서 표현하기 쉬운 경우도 있고, 또는 카테고리들간의 관계로 한 단계 위의 구조를 또 만들 수도 있습니다. 이렇게 만들어진 구조는 또 다른 구조와의 관계로 또 다른 개념을 설명할 수 있습니다. 이럴 때 카테고리간 매핑을 하는 연산을 펑터라 부릅니다. 펑터는 함수처럼 대상 하나와 다른 대상 하나를 매핑하는데서 그치는게 아니라, 모피즘도 매핑하고, 모피즘의 합성도 매핑(구조가 보존된다는 뜻입니다)합니다. 그리고 functoriality를 따라야 합니다. 분명 함수와는 조금 다른 동작을 하니 새로운 이름이 필요하지 않았을까 추측합니다. ※ 모피즘의 합성도 모피즘이니, 대상과 모피즘만 매핑해야 한다고 말해도 됩니다.

한 마디로, 값을 매핑하는 걸 function이라 하고, 구조를 매핑하면 functor라 합니다.

대부분의 텍스트는 이렇게 얘기하고 다음 스텝으로 넘어가는데, 저는 이 게 현실, 프로그래밍에 접목하면 어떤 동작을 하는지가 궁금했습니다.

※ functoriality:
(~ty가 들어가면 성질을 나타냅니다. associativity, commutativity, distributivity…근데, monadity같은 건 못봤습니다.)
항등사상에 펑터를 적용한 것과, 도착할 곳의 항등사상이 같다. Identity Law: F(id_X) = id_FX
합성한 것에 펑터를 적용하든, 펑터를 적용후 합성하든 같다. Composition Law: f:X->Y, g:Y->Z 일 때 F(g∘f) = Fg ∘ Ff

똑같은 구조와 닮은 구조

매핑이란 뭘까에 대한 상상에서 시작합니다. 그림자나, 나무 막대기로 각 관절들을 사람과 연결해 놓은 인형처럼, 한 쪽에서 움직이면 완전히 동일하게 움직이는 두 대상은, 어느 한 쪽의 움직임으로 다른 쪽의 움직임을 완벽하게 알 수 있습니다. 수학적으로 얘기하면 매핑과, 그 매핑을 정확히 뒤집은(역) 매핑이 있으면 둘은 isomorphic하다고 합니다. 언제든지 한 쪽의 정보를 이용해 다른 쪽을 알 수 있습니다. 둘은 다르지만, 연결된 관절의 움직임만 궁금하다면 둘 중 무엇을 지켜 봐도 똑같은 결과가 나와, 둘을 구별할 수 없습니다.
하지만, 세상에는 완전히 같지는 않지만, 비슷한 경우에 의미를 두면 훨씬 많은 것들을 표현할 수 있습니다. 구조가 같다보존된다는 말은, 둘이 isomorphic한 상황만 얘기하는 게 아닙니다. 저도 용어를 실수한 적이 있는데요(@기정님 감사합니다), “구조가 같다”와 “구조가 보존된다”는 차이가 있습니다. isomorphic이면 구조가 같은 것이고, homomorphic은 구조가 보존됩니다. A에 homomorphism을 적용해서 B가 됐다면, B에는 A와 같은 구조는 존재하지만, 전체 구조가 A와 같을 필요는 없습니다. homomorphic은 구조는 보존되었지만, 전체 구조는 같지 않을 수도 있습니다. isomorphic은 구조가 같으니, 당연히 구조도 보존 된, homomorphic의 특별한 경우입니다.

예를들어 A -> B, B -> C, C -> D 의 관계가 있는 걸
A, B, C 모두를 P 한 곳으로 매핑하고, 각 관계들을 P -> P에 모두 매핑해도 구조를 보존했다고 합니다.

잘 보면 구성원들끼리의 연결은 추가되거나 사라지지 않았습니다. 이렇게 구조를 보존하며 매핑하는 연산을 homomorphism이라 부릅니다. 역으로 돌아 올 수 있을지, 없을지 알 수 없습니다. 하스켈에서 만나는 대부분의 펑터는 isomorphic이 아니라 homomorphic에 관한 얘기입니다. 세상에는 똑같지 않지만, 닮은 구석들을 찾아서 (없으면 강제로 만들어서라도) 모델링하면 되는 경우가, 완전히 똑같은 경우보다 훨씬 많습니다. 예를 들면, 세세한 정보는 필요없고, 큰 정보만 보면 될 때는 세세한 정보가 지워진 것과 매핑해서, 그 구조를 살피면 훨씬 효율적입니다. 또는 매핑된 것에서는 특정 조건이 금방 눈에 띈다거나 할 수 있습니다.

IntMaybe Int는 완전히 일대일 매핑은 아니지만, Int가 가진 구조는 Maybe Int가 고스란히 가지고 있고, 즉 구조가 보존되어 있고, 추가적으로 Nothing과 관련된 구조가 있습니다.

※ 동형isomorphic, 준동형homomorphic, 모피즘(사상)morphism, 범주category를 우리말과 영어를 섞어서 계속 표기하고 있는데, 어느 쪽으로 통일하는 게 잘 읽힐지 아직 잘 모르겠습니다. 일단은 섞어서 쓴 대로 그대로 두겠습니다. 인쇄해서 불변이될 자료가 아닌 언제든 수정 가능한 블로그 글이니, 궅이 어느 한 쪽만 익숙해진 걸 가정할 필요는 없어 보입니다. 둘 다 익숙해지게 섞어 써도 나름 효과가 있겠습니다. 함자functor는 거의 쓰는 분을 못 만났습니다.

관절 연결 보존!

컨테이너란 선입견에서 벗어나자

펑터를 바라 볼 때, 컨테이너 비유로 바라보면 딱 맞아 떨어지는 경우들이 있습니다. 대표적으로 리스트 타입 같은 경우가 그렇습니다. 어떤 타입이 있고, 그 타입을 안에 담고 있는 리스트 타입과의 펑터를 예로 드는 경우가 많습니다. 제 생각은 이런 비유로 바라 보는 게, 펑터 다음 스텝(Applicatives functor, Monad)으로 갈 수록 조금씩 걸림돌이 됩니다. 컨테이너 메타포 없이 두 구조간의 매핑으로, 혹은 세세하게는 대상, 모피즘, 모피즘 합성을 다른 카테고리에 있는 것들과 매핑하는 걸 펑터로 기억하는 게 개인적으론 더 무리가 없었습니다.

하스켈에서 펑터 fmap

아마도 functor mapping의 약자쯤 될 듯한데, 딱 관련 언급을 하는 자료는 못찾았습니다. 하스켈에서 펑터는 두 가지 작업을 합쳐 펑터를 표현합니다. (실제론, 펑터 규칙functor law을 만족하게끔 구현을 잘 해햐 하는데, 하스켈에서는 이 규칙을 따르는지는 컴파일러가 체크하는 건 아니고, 프로그래머가 잘 검증해야 합니다.

IntMaybe Int와 매핑하는 걸 보면
(좀 더 풀어서 얘기하면, IntMaybe펑터를 이용해 Maybe Int에 매핑하는 걸 보면),
첫 째로 타입 생성자 MaybeInt를 매핑하고,
둘 째로 f :: Int -> Int 타입의 함수는 fmap :: (Int -> Int) -> (Maybe Int -> Maybe Int)로 매핑합니다.
f함수와 fmap f 함수를 매핑한다고 보면 됩니다.(f함수에 fmap을 적용하면 Maybe Int -> Maybe Int타입이 됩니다.)
이 둘을 합쳐 펑터라 부릅니다.

자주 보던 리스트의 mapfmap을 구현한 한 사례입니다.

이렇게 함수 변환으로 바라 보면, 상자 안에 들어 있는 무언가가 바뀌었다는 메타포보단, 구조와 구조가 매핑된다는 느낌이 들지 않나요? 그래서, 전 컨테이너 메타포로 보지 않는 게 오히려 더 편합니다.

endofunctor

endo는 안으로inside 라는 어원을 가지고 있습니다. 한 카테고리와 다른 카테고리를 매핑하는 연산을 펑터라 했는데, 이 때 출발지 카테고리와 도착지 카테고리가 동일한 경우의 매핑을 엔도펑터라 부릅니다. 하스켈은 타입들을 대상으로 하고, 함수를 모피즘으로 하는 Hask 카테고리라 부릅니다. 하스켈에서는 Hask에 있는 어떤 타입에 fmap을 적용해도 또 다시 Hask에 있는 타입 중 하나로 돌아오기 때문에 엔도펑터로 부릅니다. 즉, 하스켈에서 만나는 펑터는 모두 엔도펑터입니다. 당장 펑터를 알기 위해 필요한 용어는 아니지만, 다음 스텝을 위해 알아두고 가면 좋습니다.

비수학적 결론

펑터는 한 시스템을, 필요한 구조는 똑같이 갖고 있고, 전체 모양은 닮은 것들로 변환할 수 있는 도구입니다.

같게 보는 건 아주 중요한 도구다란 상상에 깔려 있는 생각은, 수학은 무엇과 무엇이 같다, 혹은 같게 볼 수 있다는 데에서 여러 표현이 생겨난다는 것입니다. x + 13이 같다=에서 시작해서 x2인 것에 도달하는 것처럼요.

그 다음은?

두 펑터의 관계를 보는 예를 들어보겠습니다.

길이를 재는 자가 있는데, 눈금이 1cm로 촘촘한 자 Dense와, 눈금이 2cm로 듬성 듬성있는 자 Sparse가 있습니다. 우리가 필요한 정밀도가 2cm면 충분할 때는 Sparse로 재면 됩니다. Dense로 재면 3cm인데, Sparse로 재면 3cm는 잴 수가 없으니 큰 쪽 눈금을 읽는다고 정해서 4cm로 읽는 걸로 약속을 정하겠습니다. 어느 자로 쟀는지 표시하기 위해 Int에 타입 Dense, Sparse를 씌우면, 이 약속이 바로

펑터 :: Int -----> Dense Int
펑터 :: Int -----> Sparse Int

입니다(물론 엄밀하게는 더 따져야 할 것들이 있습니다.). 잘 보면, Dense에서 Sparse로 매핑 가능하지만, Sparse에선 Dense로 돌아 올 수 없습니다. 4cm는 3cm일 수도 있고, 4cm일 수도 있습니다. Dense에서 Sparse로 가는 isomorphic이 아니라, homomorphic 동작입니다. 그런데, 이런 상황에서 돌아올 수 있는 경우가 있습니다.

※ 펑터간 매핑을 자연 변환Natural Transformation이라 합니다.

임의로 1cm 정도의 오차는 허용하고, 큰 수로 항상 매핑하겠다 정하면 Sparse에서 Dense로 돌아 올 수 있습니다. 그냥은 안되지만, 어떤 조건이 있으면 가능합니다. 조건은 변환 함수(자연 변환)로 나타납니다.

펑터 적용 후 이전으로 돌아 갈 수 없는 상황에서, 적당한 변환 함수가 있으면 같게 볼 수 있게 되었습니다. 이 걸 읽는 방법은 두 가지인데, 하나는 대상을 변환 시켜 같아지게 만들었다 볼 수 있고, 다른 한가지 방법은 같게 보는 눈을, 판단을 바꾼 걸로 볼 수 있습니다. 아주 인포멀하게 얘기하면

A = B

가 안되는 상황에서 AB를 바꾸는 게 아니라, =을 바꿨다고 볼 수 있습니다.

이 이야기는 모노이드, 모나드로 연결됩니다.

Dense도, SparseInt를 담는 컨테이너로 읽는 분들도 있습니다. 저는 구조간 매핑으로 읽는 게 더 편합니다.

다른 교재나 텍스트들을 그대로 번역하거나 옮겨온 것이 아닌 제 생각, 상상을 쓰는지라, 정답 지식이 아니라, 다른데서 볼 수 없었던 힌트를 얻는 정도 글이 됐으면 좋겠습니다.

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