Skip to content

Latest commit

 

History

History
177 lines (112 loc) · 15 KB

01-category-the-essence-of-composition.md

File metadata and controls

177 lines (112 loc) · 15 KB

범주: 합성의 본질

저는 제 이전 게시물인 프로그래머를 위한 범주론의 서문에 대한 긍정적인 반응에 압도되었습니다. 동시에 사람들이 저에게 얼마나 큰 기대를 하고 있는지를 알았기 때문에 저를 두렵게 했습니다. 제가 어떤 글을 써도 독자들이 실망할까 봐 두렵습니다. 어떤 독자는 이 책이 실용적이기를 원하고 어떤 독자는 더 추상적이기를 원합니다. 어떤 사람은 C++를 싫어하고 Haskell로 된 모든 예제를 원하고, 다른 사람은 Haskell을 싫어하고 Java 예제를 요구할 수 있습니다. 그리고 저는 설명을 하는 속도가 누군가에게는 너무 느리고 누군가에게는 너무 빠를 것이라는 것을 알고 있습니다. 이것은 완벽한 책은 아닙니다. 이것으로 타협이 될 것입니다. 제가 바랄 수 있는 것은 제가 깨달은 것 일부를 공유할 수 있다는 것뿐입니다. 기초부터 시작하겠습니다.

범주는 당황스러울 정도로 단순한 개념입니다. 범주는 객체와 객체 사이를 이동하는 화살표로 구성됩니다. 그래서 범주는 그림으로 표현하기 쉽습니다. 객체는 원이나 점으로 그릴 수 있으며, 화살표는 화살표입니다. (다양성을 위해 가끔 물건을 돼지처럼, 화살을 불꽃처럼 그립니다) 그러나 범주의 본질은 합성입니다. 또는 원하는 경우 합성의 본질은 범주입니다. 화살표 합성이므로 객체 A에서 객체 B로의 화살표와 객체 B에서 객체 C로의 또 다른 화살표가 있는 경우 A에서 C로 가는 화살표(합성)가 있어야 합니다.

범주에서 A에서 B로 가는 화살표와 B에서 C로 가는 화살표가 있으면
A에서 C로 직접 향하는 화살표도 있어야 합니다.
이 다이어그램은 항등 사상(identity morphisms)이 없기 때문에 전체 범주가 아닙니다. (나중에 참조)

함수로서의 화살표

이것들이 이미 너무 추상적인 넌센스였나요? 절망하지 마세요. 구체적인 이야기를 해보겠습니다. 사상이라고도 하는 화살표를 함수로 생각하세요. 타입 A의 인자를 받아 B를 반환하는 함수 f가 있습니다. B를 인자로 받고 C를 반환하는 또 다른 함수 g가 있습니다. f의 결과를 g에 전달하여 합성할 수 있습니다. A를 받고 C를 반환하는 새로운 함수를 방금 정의했습니다.

수학에서 이러한 합성은 함수 사이의 작은 원(g∘f)으로 표현됩니다. 조합의 오른쪽에서 왼쪽 순서에 유의해야 합니다. 어떤 사람들에게는 이것이 혼란스러울 수 있습니다. 아래와 같이 Unix의 파이프 표기법에 익숙할 것입니다.

lsof | grep Chrome

또는 F#의 갈매기 모양의 >>, 둘 다 왼쪽에서 오른쪽으로 이동합니다. 그러나 수학과 Haskell 함수에서는 오른쪽에서 왼쪽으로 합성합니다. g∘f를 "f 후에 g"로 읽으면 도움이 됩니다.

C 코드를 작성하여 이것을 더욱 명시적으로 만들어 보겠습니다. A 타입의 인자를 받고 B 타입의 값을 반환하는 함수 f가 하나 있습니다.

B f(A a);

그리고 B타입의 인자를 받고 C 타입의 값을 반환하는 또 다른 함수가 있습니다.

C g(B b);

이것들의 합성은 아래와 같습니다.

C g_after_f(A a)
{
    return g(f(a));
}

여기에서 다시 오른쪽에서 왼쪽으로 합성되는 g(f(a));를 C 코드에서 볼 수 있습니다.

C++ 표준 라이브러리에는 두 개의 함수를 사용하고 합성을 반환하는 템플릿이 있다고 말하고 싶지만 없다는 것을 말하고 싶습니다. 변화를 위해 하스켈로 같은 내용을 작성해 보겠습니다. 여기에 A를 받아 B를 반환하는 함수 선언이 있습니다.

f :: A -> B

비슷하게 B를 받아 C를 반환하는 함수 선언이 있습니다.

g :: B -> C

이것들의 합성은 아래와 같습니다.

g . f

Haskell이 얼마나 간단한지 알게 되면 C++에서 간단한 함수형 개념을 표현할 수 없다는 것이 조금 당황스러울 수 있습니다. 사실 Haskell은 유니코드 문자를 사용할 수 있게 해주므로 아래와 같이 합성을 작성할 수 있습니다.

g  f

유니코드 이중 콜론과 화살표를 사용할 수도 있습니다.

f  A  B

그래서 여기 첫 번째 Haskell 수업이 있습니다. 이중 콜론은 "다음 타입을 가짐"을 의미합니다. 함수 타입은 두 타입 사이에 화살표를 삽입하여 생성됩니다. 두 함수 사이에 마침표(또는 유니코드 원)를 삽입해 두 함수를 합성합니다.

합성의 속성

모든 범주가 만족해야 하는 두 가지 매우 중요한 속성이 있습니다.

  1. 합성은 결합성이 있습니다. 합성할 수 있는 세 가지 사상인 f, g, h가 있는 경우(즉, 객체가 종단 간 일치합니다) 합성을 하기 위해 괄호가 필요하지 않습니다. 수학적으로 표현한다면 다음과 같이 표현할 수 있습니다.

$h∘(g∘f) = (h∘g)∘f = h∘g∘f$

Haskell의 의사 코드로는 아래와 같이 표현할 수 있습니다.

f :: A -> B
g :: B -> C
h :: C -> D
h . (g . f) == (h . g) . f == h . g . f

(저는 함수에 대해 등식이 정의되지 않았기 때문에 "의사"라고 표현했습니다.)

결합성은 함수를 다룰 때 매우 분명하지만 다른 범주에서는 그렇게 분명하지 않을 수 있습니다.

  1. 모든 객체 A에는 합성 단위인 화살표가 있습니다. 이 화살표는 객체 자신을 가리킵니다. 합성 단위라는 것은 각각 A에서 시작하거나 A에서 끝나는 화살표로 합성될 때 동일한 화살표를 반환한다는 것을 의미합니다. 객체 A에 대한 단위 화살표를 idA(A에 항등, identity on A)라고 부릅니다. 수학적 표현에서 f가 A에서 B로 이동한다면 아래와 같이 표현할 수 있습니다.

$f∘id_A = f$ 그리고 $id_B∘f = f$

함수를 다룰 때, 항등 화살표는 인자를 그대로 반환하는 항등함수로 구현됩니다. 구현은 모든 타입에 대해 동일합니다. 즉, 이 함수는 다형성을 갖는다는 것을 의미합니다. C++에서는 다음과 같은 템플릿으로 표현할 수 있습니다.

template<class T> T id(T x) { return x; }

물론 C++에서는 전달하는 내용뿐만 아니라 방법(값, 참조, 상수 참조, 이동 등)도 고려해야 하기 때문에 그렇게 간단하지 않습니다.

Haskell에서는 항등 함수는 Prelude라고 불리는 표준 라이브러리의 일부입니다. 선언과 정의는 아래와 같습니다.

id :: a -> a
id x = x

보시다시피, Haskell의 다형성 함수는 식은 죽 먹기입니다. 선언에서 타입을 타입 변수로 바꾸기만 하면 됩니다. 여기에는 트릭이 있습니다. 구체적인 타입의 이름은 항상 대문자로 시작하고 타입 변수의 이름은 소문자로 시작합니다. 그래서 여기서 a는 모든 타입을 의미합니다.

Haskell 함수 정의는 함수 이름과 형식 매개변수로 구성되어 있습니다. 여기서는 x 하나만 있습니다. 함수의 본문은 등호를 따릅니다. 이 간결함은 종종 새로운 사람들에게 충격을 주지만 완벽하게 이해된다는 것을 금세 알게 될 것입니다. 함수 정의와 함수 호출은 함수형 프로그래밍의 핵심이므로 최소한으로 줄어듭니다. 인자 목록 주위에 괄호가 없을 뿐 아니라 인자 사이에 쉼표도 없습니다. (나중에 여러 인자를 갖는 함수를 정의할 때 볼 수 있습니다)

함수의 본문은 항상 표현식입니다. 함수에는 문이 존재하지 않습니다. 함수의 결과는 여기서는 x인 표현식 입니다.

이것으로 두 번째 Haskell 수업을 마칩니다.

항등 조건은 의사 Haskell 코드로 아래와 같이 작성될 수 있습니다.

f . id == f
id . f == f

당신은 스스로 왜 누군가 아무것도 하지 않는 항등함수에 신경을 쓰지 않을까? 하는 질문을 할 수 있습니다. 그렇다면 우리는 왜 숫자 0에 신경을 써야 할까요? 0은 무의미한 상징입니다. 고대 로마인들은 0이 없는 숫자 체계를 가지고 있었고 훌륭한 도로와 수로를 건설할 수 있었고 그중 일부는 오늘날까지 남아 있습니다.

0 또는 항등함수와 같은 중립 값은 기호 변수로 작업할 때 매우 유용합니다. 그래서 로마인들은 대수학을 잘하지 못했지만, 0의 개념에 익숙한 아랍인과 페르시아인은 대수학을 잘했습니다. 따라서 항등 함수는 고차 함수에 대한 인수 또는 반환으로 매우 편리하게 사용됩니다. 고차 함수는 함수의 상징적 조작을 가능하게 하는 것입니다. 고차 함수는 함수의 대수입니다.

요약하자면 범주는 객체와 화살표 (사상)으로 구성됩니다. 화살표는 합성될 수 있으며 합성은 결합성이 있습니다. 모든 객체에는 합성에서 단위 역할을 하는 항등 화살표가 있습니다.

합성은 프로그래밍의 본질입니다.

함수형 프로그래머는 문제에 접근하는 독특한 방법을 가지고 있습니다. 함수형 프로그래머는 매우 선(Zen)과 같은 질문을 하는 것으로 시작합니다. 예를 들어 대화형 프로그램을 설계할 때 "상호 작용이란 무엇일까?"와 같이 질문할 것입니다. Conway의 Game of Life를 구현할 때 그들은 아마도 삶의 의미에 대해 생각할 것입니다. 이러한 정신으로 저는 "프로그래밍이란 무엇일까?"라고 질문하겠습니다. 가장 기본적인 수준에서 프로그래밍은 컴퓨터에 무엇을 하라고 지시하는 것입니다. "메모리 주소 x의 내용을 가져와 레지스터 EAX의 내용에 추가하세요." 그러나 우리가 조립식으로 프로그래밍할 때도 우리가 컴퓨터에 주는 명령은 더 의미 있는 표현입니다. 우리는 사소한 문제를 해결하고 있습니다. (사소한 문제라면 컴퓨터의 도움이 필요하지 않을 것입니다) 문제를 어떻게 해결할 수 있을까요? 우리는 더 큰 문제를 더 작은 문제로 나눕니다. 더 작은 문제가 여전히 크다면 더 나누는 식으로 진행합니다. 마지막으로 모든 작은 문제를 해결하는 코드를 작성합니다. 그 후에 프로그래밍의 본질이 나옵니다. 우리는 더 큰 문제에 대한 해결책을 만들기 위해 이런 코드 조각을 합성합니다. 조각들을 다시 모을 수 없다면 문제를 나누는 것은 의미가 없습니다.

이런 계층적 분해 및 재구성 과정은 컴퓨터에 의해 우리에게 강요되지 않습니다. 이것들은 사람의 정신적 한계를 반영합니다. 우리의 뇌는 한 번에 적은 수의 개념만 다룰 수 있습니다. 심리학에서 가장 많이 인용되는 논문 중 하나인 Magical Number Seven, Plus or Minus Two는 우리가 마음 속에 7 ± 2개의 "덩어리" 정보만 유지할 수 있다고 가정했습니다. 인간의 단기 기억에 대한 우리의 이해의 세부 사항은 변할 수 있지만, 우리는 그것이 제한적이라는 것을 확실히 알고 있습니다. 결론은 우리가 복잡한 객체나 스파게티 코드를 다룰 수 없다는 것입니다. 우리에게 필요한 것은 잘 짜여진 프로그램이 보기에 좋아서가 아니라 뇌가 효율적으로 처리할 수 없기 때문입니다. 우리는 종종 코드의 일부를 우아하거나 아름답다고 설명하지만 실제로 의미하는 것은 제한된 인간의 마음으로 처리하기 쉽다는 것입니다. 우아한 코드는 우리의 정신 소화 시스템이 이해할 수 있도록 적절한 크기와 적절한 수의 덩어리를 생성합니다.

그렇다면 프로그램 합성에 적합한 덩어리는 무엇일까요? 넓이는 부피보다 느리게 증가해야 합니다. (저는 이 비유를 좋아합니다. 왜냐하면 기하학적인 물체의 넓이는 부피보다 느리게 그 크기의 제곱에 따라 증가하기 때문입니다. 부피는 그 크기의 3제곱에 따라 증가하기 때문입니다) 넓이는 덩어리를 구성하는 데 필요한 정보입니다. 부피는 이를 구현하는 데 필요한 정보입니다. 아이디어는 덩어리가 구현되면 구현 세부 사항을 잊고 다른 덩어리와 상호 작용하는 방식에 집중할 수 있다는 것입니다. 객체 지향 프로그래밍에서 넓이는 객체의 클래스 선언 또는 객체의 추상 인터페이스입니다. 함수형 프로그래밍에서는 함수 선언입니다. (저는 일을 조금 단순화하고 있지만 이것이 중요한 부분입니다.)

범주론은 우리가 사물의 내부를 보는 것을 적극적으로 방해한다는 점에서 극단적입니다. 범주론의 객체는 추상적이고 모호한 객체입니다. 그것에 대해 알 수 있는 것은 다른 객체와 어떻게 관련되어 있는지, 즉 화살표를 사용해 객체와 연결하는 방법뿐입니다. 이것은 인터넷 검색 엔진이 들어오는 링크와 나가는 링크를 분석해 웹 사이트의 순위를 매기는 방식입니다. (속임수인 경우는 제외) 객체 지향 프로그래밍에서 이상적인 객체는 화살표 역할을 하는 메서드와 함께 추상 인터페이스(순수한 넓이, 부피 없음)를 통해서만 볼 수 있습니다. 다른 객체와 합성하는 방법을 이해하기 위해서는 객체의 구현을 파헤쳐야 하는 순간 프로그래밍 패러다임의 이점을 잃게 됩니다.

도전

  1. 가능한 한 가장 좋아하는 언어로 항등함수를 구현해보세요. (좋아하는 언어가 Haskell인 경우 두 번째로 좋아하는 언어로 구현해보세요)
  2. 선호하는 언어로 함수 합성 기능을 구현해보세요. 두 개의 함수를 인자로 받고 두 함수를 합성한 함수를 반환하면 됩니다.
  3. 합성된 함수가 항등성을 따르는지 테스트하는 프로그램을 작성해보세요.
  4. World-wide web은 어떤 의미에서 범주일까요? 링크가 사상인가요?
  5. 페이스북은 사람을 객체로, 우정을 사상으로 하는 범주인가요?
  6. 방향이 있는 그래프는 언제 범주가 되나요?

⬅ 뒤로가기 / 다음으로 ➡

Translated by @Minsu Kim