And Brain said,

함수적인, 너무나 함수적인 - 4. 펑터 (Functor)와 엔도펑터(Endo Functor) 본문

IT/함수적인, 너무나 함수적인

함수적인, 너무나 함수적인 - 4. 펑터 (Functor)와 엔도펑터(Endo Functor)

The Man 2023. 4. 22. 11:01
반응형

https://en.wikipedia.org/wiki/Functor

Index
0. 함수형 프로그래밍
1. 람다 대수 (Lambda Calculus)와 합성함수
2. 범주론 (Category Theory)
3. 모노이드 (Monoide)
4. 펑터 (Functor)와 엔도펑터(Endo Functor)
5. 모나드 (Monad)
6 함수형 프로그래밍의 고급 기법

 
그래도 여전히 두려워하지 마세요. 여기까지 오신 여러분은 충분히 펑터를 견뎌낼 수 있습니다.
 

펑터 (Functor)

펑터? 함수형 프로그래머가 펑터는 왜 알아야 할까요?
먼저, 펑터를 사용하면 내부의 값을 변경하지 않고 외부에서 연산을 적용할 수 있습니다. 이렇게 함으로써 함수형 프로그래밍의 주요 원칙 중 하나인 불변성(immutability)을 유지할 수 있습니다.
 
또한, 펑터를 사용하면 값의 타입을 변환할 수 있습니다. 일반적인 함수 적용과 달리, 펑터는 적용할 함수의 타입 변환을 처리할 수 있으며, 다양한 타입 간 변환 작업을 일반화하여 재사용 가능한 코드를 작성할 수 있습니다.

아직 와닿지 않으실 겁니다. 괜찮습니다. 자, 그럼 본격적으로 펑터를 시작해봅시다. 펑터란 어떤 범주의 대상과 사상을 다른 범주의 대상과 사상으로 대응 시키는 구조를 말합니다. 범주의 정의는 벌써 세 번째지만 다시 한 번 상기시켜봅시다.
 

범주란, 대상과 사상의 모음이다.

 
그럼, 두 개의 범주 D와 E를 고려해 봅시다.

범주 D : 대상: A, B, C  //  사상: f: A -> B, g: B -> C, h: A -> C (여기서 h = g ∘ f)
범주 E : 대상: X, Y, Z  //  사상: F(f): X -> Y, F(g): Y -> Z, F(h): X -> Z (여기서 F(h) = F(g) ∘ F(f))
 
여기서 펑터 F는 범주 D의 대상과 사상을 범주 E의 대상과 사상으로 대응시키는 규칙입니다. 펑터 F는 다음과 같은 조건을 충족합니다.

대상 대응: 펑터 F는 범주 D의 대상 A, B, C를 범주 E의 대상 X, Y, Z로 대응시킵니다. 예를 들어, F(A) = X, F(B) = Y, F(C) = Z입니다.

사상 대응: 펑터 F는 범주 D의 사상 f, g, h를 범주 E의 사상 F(f), F(g), F(h)로 대응시킵니다. 예를 들어, F(f)는 X -> Y로 가는 사상, F(g)는 Y -> Z로 가는 사상, F(h)는 X -> Z로 가는 사상입니다.

항등 사상 보존: 펑터 F는 범주 D의 모든 대상 A, B, C에 대해 항등 사상을 보존합니다. 예를 들어, 범주 D의 대상 A의 항등 사상은 id_A입니다. 펑터 F는 이를 범주 E의 대상 X의 항등 사상 id_X와 같게 만듭니다. 즉, F(id_A) = id_X입니다. 이는 다른 대상 B와 C에 대해서도 동일하게 적용됩니다.

합성 사상 보존: 펑터 F는 범주 D의 모든 사상 f, g, h에 대해 합성 사상을 보존합니다. 예를 들어, 범주 D에서 h = g ∘ f라고 가정합니다. 이 경우, 펑터 F는 범주 D에서 F(h) = F(g) ∘ F(f)를 만족해야 합니다. 즉, 범주 D의 사상의 합성을 범주 E의 사상의 합성으로 올바르게 변환합니다.

 
 
더 구체적인 예시를 들어볼까요? 범주 D를 집합의 범주로 생각해 봅시다. 이 경우, 대상은 집합이며 사상은 집합 간의 함수입니다. 범주 E도 집합의 범주로 생각하겠습니다. 여기서 펑터 F를 "집합 A의 모든 부분집합의 집합"으로 정의해봅시다. 이 펑터 F는 다음과 같이 작동합니다.

대상 대응: 범주 D의 대상 집합 A를 대응시키면, 범주 E의 대상 집합은 A의 모든 부분집합의 집합, 즉 멱집합 P(A)가 됩니다.
사상 대응: 범주 D의 사상(함수) f를 대응시키면, 범주 E의 사상 F(f)는 A의 부분집합을 그에 상응하는 B의 부분집합으로 대응시키는 함수가 됩니다.
예를 들어, 범주 D의 대상 집합 A = {1, 2}, B = {3, 4}이고 사상 f는 A의 원소 1을 B의 원소 3으로, 원소 2를 원소 4로 대응시키는 함수라고 가정합시다.

반응형


이 경우, 펑터 F에 의해 범주 E의 대상 집합은 다음과 같습니다.

F(A) = P(A) = {∅, {1}, {2}, {1, 2}}
F(B) = P(B) = {∅, {3}, {4}, {3, 4}}

펑터 F에 의해 범주 E의 사상 F(f)는 A의 부분집합을 B의 부분집합으로 대응시키는 함수가 됩니다. 예를 들어, F(f)의 대응은 다음과 같이 정의될 수 있습니다.

F(f)(∅) = ∅
F(f)({1}) = {3}
F(f)({2}) = {4}
F(f)({1, 2}) = {3, 4}

이처럼 펑터 F는 범주 D의 대상과 사상을 범주 E의 대상과 사상으로 대응시키면서 범주 D의 구조와 패턴을 보존합니다.
 
이제, 이 펑터라는 개념을 프로그래밍에 녹여봅시다.

// Maybe 타입은 Just와 Nothing 두 가지 경우로 나뉩니다.
// 펑터의 대상
type Maybe<T> = Just<T> | Nothing;

// Just 클래스는 값을 포함하는 경우를 나타냅니다.
class Just<T> {
  constructor(public value: T) {}
}

// Nothing 클래스는 값을 포함하지 않는 경우를 나타냅니다.
class Nothing {
  constructor() {}
}

// map 함수는 Maybe 펑터를 구현합니다.
// 이 함수는 범주 C의 사상(함수)을 범주 D의 사상(함수)으로 변환하는 역할을 합니다.
// 여기서 범주 C의 대상은 원래의 값(T)이고, 범주 D의 대상은 변환된 값(U)입니다.
// 대상 대응: Maybe<T> 타입의 값을 Maybe<U> 타입의 값으로 변환합니다.
// 사상 대응: 주어진 함수 fn을 적용하여 T 타입의 값을 U 타입의 값으로 변환합니다.
// 항등 사상 보존: 입력이 Nothing 인스턴스인 경우, 아무 변환도 적용되지 않고 그대로 Nothing 인스턴스를 반환합니다.
// 합성 사상 보존: 두 개의 함수를 연속적으로 적용할 때, 결과가 동일하게 됩니다.
function map<T, U>(maybe: Maybe<T>, fn: (value: T) => U): Maybe<U> {

  // maybe가 Just 인스턴스인 경우, 즉 값이 포함된 경우
  if (maybe instanceof Just) {
    // Just 인스턴스의 값을 변환한 후 새로운 Just 인스턴스를 반환합니다.
    // 이 과정은 범주 C의 대상(T)을 범주 D의 대상(U)으로 변환합니다.
    return new Just(fn(maybe.value));
    
  } else {
    // maybe가 Nothing 인스턴스인 경우, 즉 값이 없는 경우
    // 변환 없이 그대로 Nothing 인스턴스를 반환합니다.
    // 이 과정은 범주 C와 D의 항등 사상을 보존합니다.
    return new Nothing();
  }
}

 
자, 이제 이를 활용해 볼까요?
 
먼저, 숫자를 제곱하는 함수를 사용하는 Maybe 펑터 예제입니다.

// 이 함수는 주어진 Maybe<number>를 받아서 숫자를 제곱하는 함수입니다.
// 대상 대응: Just<number>와 Nothing의 두 가지 경우를 모두 처리합니다.
// 사상 대응: map 함수를 사용하여 숫자를 제곱하는 함수를 적용합니다.
// 항등 사상 보존: maybe가 Nothing인 경우, 아무 변환도 적용되지 않고 그대로 반환됩니다.
// 합성 사상 보존: 다른 함수와 합성할 때 순서대로 적용되고, 결과가 동일합니다.
const maybeSquare = (maybe: Maybe<number>): Maybe<number> => {
  return map(maybe, (x) => x * x);
};

const maybeNumber: Maybe<number> = new Just(4);
const maybeSquareResult = maybeSquare(maybeNumber);
console.log(maybeSquareResult); // 출력: Just { value: 16 }

 
다음은, 문자열을 대문자로 변환하는 함수를 사용하는 Maybe 펑터 예제입니다.

// 이 함수는 주어진 Maybe<string>를 받아서 문자열을 대문자로 변환하는 함수입니다.
// 대상 대응: Just<string>와 Nothing의 두 가지 경우를 모두 처리합니다.
// 사상 대응: map 함수를 사용하여 문자열을 대문자로 변환하는 함수를 적용합니다.
// 항등 사상 보존: maybe가 Nothing인 경우, 아무 변환도 적용되지 않고 그대로 반환됩니다.
// 합성 사상 보존: 다른 함수와 합성할 때 순서대로 적용되고, 결과가 동일합니다.
const maybeToUpper = (maybe: Maybe<string>): Maybe<string> => {
  return map(maybe, (s) => s.toUpperCase());
};

const maybeString: Maybe<string> = new Just("hello");
const maybeUpperResult = maybeToUpper(maybeString);
console.log(maybeUpperResult); // 출력: Just { value: 'HELLO' }

 
다음은, 배열의 첫 번째 요소를 가져오는 함수를 사용하는 Maybe 펑터 예제입니다.

// 이 함수는 주어진 배열의 첫 번째 요소를 가져오는 함수입니다.
// 배열의 길이가 0보다 크면 Just 인스턴스를 반환하고, 그렇지 않으면 Nothing 인스턴스를 반환합니다.
// 대상 대응: 배열의 길이에 따라 Just<T> 또는 Nothing 인스턴스를 반환합니다.
// 사상 대응: 배열의 첫 번째 요소를 반환하는 함수를 적용합니다.
// 항등 사상 보존: 배열의 첫 번째 요소가 없을 경우 Nothing 인스턴스를 반환하며, 아무 변환도 적용되지 않습니다.
// 합성 사상 보존: 다른 함수와 합성할 때 순서대로 적용되고, 결과가 동일합니다.
const maybeHead = <T>(arr: T[]): Maybe<T> => {
  return arr.length > 0 ? new Just(arr[0]) : new Nothing();
};

const arr = [1, 2, 3, 4, 5];
const maybeHeadResult = maybeHead(arr);
console.log(maybeHeadResult); // 출력: Just { value: 1 }

 
마지막으로, JSON 문자열을 파싱하는 함수를 사용하는 Maybe 펑터 예제입니다.

// 이 함수는 주어진 문자열을 JSON 객체로 파싱하는 함수입니다.
// 문자열이 유효한 JSON 형식이면 Just 인스턴스를 반환하고, 그렇지 않으면 Nothing 인스턴스를 반환합니다.
// 대상 대응: 주어진 문자열이 유효한 JSON 형식일 경우 Just<T> 인스턴스를 반환하며, 그렇지 않을 경우 Nothing 인스턴스를 반환합니다.
// 사상 대응: 주어진 문자열을 JSON 객체로 파싱한 결과를 반환합니다.
// 항등 사상 보존: 문자열이 유효한 JSON 형식이 아닐 경우 Nothing 인스턴스를 반환하며, 아무 변환도 적용되지 않습니다.
// 합성 사상 보존: 다른 함수와 합성할 때 순서대로 적용되고, 결과가 동일합니다.
const maybeParseJSON = (jsonString: string): Maybe<object> => {
try {
return new Just(JSON.parse(jsonString));
} catch {
return new Nothing();
}
};

const jsonString = '{"key": "value"}';
const maybeJSONResult = maybeParseJSON(jsonString);
console.log(maybeJSONResult); // 출력: Just { value: { key: 'value' } }

 

엔도펑터(Endo Functor)

이제, 펑터에서 한걸음만 더 나아가서 엔도펑터에 대해서 배워봅시다.
 
엔도펑터는 펑터의 특별한 경우입니다. '엔도'라는 단어는 그리스어 'endo'에서 유래되었으며 '내부의'나 '내부로'라는 뜻을 가지고 있습니다. 따라서 엔도펑터(endofunctor)란 범주 내에서 작동하는 펑터를 의미합니다. 다른 범주로 대응시키는 일반적인 펑터와는 달리, 엔도펑터는 같은 범주 내에서 작동한다는 점에서 차이가 있습니다.
 
아까 보았던 범주 D로 설명해봅시다.
 
범주 D: 대상: A, B, C // 사상: f: A -> B, g: B -> C, h: A -> C (여기서 h = g ∘ f)

엔도펑터는 범주 D의 대상과 사상을 동일한 범주 D 내에서 대응시키는 규칙입니다. 예를 들어, 엔도펑터 E를 "각 대상을 두 배로 만드는" 규칙이라고 가정해봅시다. 이 경우, 엔도펑터 E는 다음과 같이 작동합니다.

대상 대응: 범주 D의 대상 A를 대응시키면, 범주 D의 대상은 A의 두 배인 E(A)가 됩니다. B와 C에 대해서도 동일한 방식으로 작동합니다.

사상 대응: 범주 D의 사상 f를 대응시키면, 범주 D의 사상 E(f)는 A의 두 배를 B의 두 배로 대응시키는 함수가 됩니다. g와 h에 대해서도 동일한 방식으로 작동합니다.

이렇게 엔도펑터는 범주 내의 대상과 사상을 변환하면서 그 범주의 구조와 패턴을 보존합니다.
 
함수형 프로그래밍에서, 엔도펑터는 프로그래밍 언어의 타입과 함수를 변환하면서 구조와 패턴을 보존하는 일종의 "타입 안에서의 함수 변환"을 의미합니다.
 

// Endo는 함수를 값으로 가지며, 같은 타입의 인자를 받아 같은 타입의 결과를 반환하는 함수를 나타냅니다.
type Endo<A> = (value: A) => A;

// 이 함수는 Endo 타입의 함수와, 변환 함수를 인자로 받아 새로운 Endo 타입의 함수를 반환합니다.
function fmap<A>(endo: Endo<A>, fn: Endo<A>): Endo<A> {
  return (value: A) => fn(endo(value));
}

 
엔도펑터는 같은 타입의 인자를 받아 같은 타입의 결과를 반환하는 함수입니다. fmap 함수는 이 엔도펑터와 함께 사용되어, 새로운 타입의 엔도펑터를 생성합니다.
 

// 문자열을 대문자로 변환하는 엔도펑터
const toUpperCase: Endo<string> = (value: string) => value.toUpperCase();

// 문자열에 느낌표를 추가하는 엔도펑터
const addExclamation: Endo<string> = (value: string) => value + '!';

// 두 엔도펑터를 조합합니다.
const shout: Endo<string> = fmap(toUpperCase, addExclamation);

// 조합된 엔도펑터를 사용합니다.
console.log(shout("hello")); // 출력: "HELLO!"


이 예제에서 fmap은 두 개의 엔도펑터를 받아 새로운 엔도펑터를 생성합니다. 각각의 엔도펑터는 문자열을 받아 문자열을 반환합니다.
 

끝으로,

이 글에서는 펑터와 펑터의 특별한 경우인 엔도펑터에 대해서 알아보았습니다. 펑터와 엔도펑터는 함수형 프로그래밍의 핵심 개념 중 하나입니다. 이들은 코드의 재사용성과 확장성을 높여주며, 복잡한 프로그램을 더 간결하고 이해하기 쉬운 형태로 만들어 줍니다.

오늘은 꽤나 어려웠을 수 있습니다. 전부 이해하셨다면, 훌륭하십니다. 그렇게 꾸준히 공부해 나가시길 바랍니다. 하지만 이해가 안 가셨어도 괜찮습니다. 익숙하지 않아서 어려운 것입니다. 계속해서 연습하고 익숙해지면, 그렇게 어렵지 않았다는 것을 깨닫게 될 것입니다. 다음은 함수적인, 너무나 함수적인 프로그래밍의 정수, 모나드를 배워봅시다.
 

Thanks for watching, Have a nice day.

 

References

https://ko.wikipedia.org/wiki/%ED%95%A8%EC%9E%90_(%EC%88%98%ED%95%99)
https://medium.com/@jooyunghan/functor-and-monad-examples-in-plain-java-9ea4d6630c6
https://www.adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
https://typelevel.org/cats/typeclasses/functor.html

반응형
Comments