Skip to content

Latest commit

 

History

History
337 lines (246 loc) · 18.3 KB

File metadata and controls

337 lines (246 loc) · 18.3 KB

책 내용에서 이해하지 못한 부분은 제외하고 이해한 부분을 중심으로 정리했습니다.
연한 텍스트는 인용구, 기본 텍스트는 저의 생각입니다. :)


CHAPTER01. 카테고리: 합성의 본질

카테고리는 대상과 그 사이를 이어주는 화살표로 구성된다. 따라서 카테고리의 본질은 합성이다. 반드시 대상 A에서 C로 향하는 화살표, 즉 A->B와 B->C의 합성이 존재해야한다.

합성을 만족시키기 위해서는 두가지 조건이 필요하다. 바로 결합법칙 성립과 모든 대상 A에는 항등 개념을 가진 화살표가 존재해야한다는 것이다.

함수의 본문은 항상 표현식(expression)이며, 문(statement)은 존재하지 않는다. (반환값이 반드시 있다.)

즉 우아한 코드는 우리의 뇌가 처리할 수 있는 적절한 크기, 적절한 개수의 청크를 만들어내는 것이라고 볼 수 있다. 그렇다면 적합한 청크는 무엇일까? 먼저 청크의 면적은 청크의 부피보다 느리게 증가해야한다. (면적: 외부에서 필요한, 합성하는데 필요한 정보, 부피: 구체적인 구현 정보)

객체지향에서 면적은 클래스나 인터페이스이지만, 함수형에서는 함수의 선언이라고 볼 수 있다.

const add = (a, b) => a + b;
  • 면적: add 함수의 입력과 출력은 면적이라고 볼 수 있다. 즉, 두 개의 숫자를 받아 그 합을 반환하는 역할이 면적이다.
  • 부피: add 함수 내부에서 실제로 a + b라는 덧셈이 수행되는 연산로직 자체는 부피라고 볼 수 있다.

CHAPTER02. 타입과 함수

타입체킹은 무의미하게 작성된 프로그램에 대한 하나의 방어막이다. 정적체크되는 타입 불일치는 컴파일 타임에 발견되기 때문에 굳이 실행해 보지않더라도 잘못된 프로그램을 잡아낸다.

임의의 두 화살표를 아무렇게나 합성할 수 있는 것은 아니다. 한 화살표의 목표인 대상은 다음 화살표의 출발 대상과 동일해야한다. 즉 하ㅂ성이 제대로 되려면 양 끝단이 일치해야 한다는 것이며 타입시스템이 강력할수록 이러한 일치여부를 잘 표현하고 검증할 수 있다.

우리가 명령형 언어에서 함수라고 부른 것들은 수학자들이 함수라고 부르는 것과는 약간 다른 개념이다. 수학적 함수는 그저 어떠한 값들 간의 사상(Mapping)일 뿐이기 때문이다.


CHAPTER03. 다양한 카테고리들

Thin 카테고리는 대상 a에서 대상 b로 가는 사상이 최대 1개만 존재하는 카테고리이다. 원순서 집합이 Thin 카테고리에 해당한다. (원순서 집합: 작거나 같다"는 관계를 나타내는 집합 a ≤ b)

Hom 집합은 두 대상 사이의 모든 사상들의 집합이다. 예를 들어, 카테고리 C에서 대상 a와 b가 있을 때, Hom 집합은 a에서 b로 향하는 모든 사상들의 집합을 의미한다. 이를 수학적으로 C(a, b) 또는 Hom(C, a, b) 로 표현한다.

모노이드는 이항연산을 가진 집합으로 정의된다. 이 이항연산은 결합법칙을 만족해야하며 연산에 대해 상등원 같은 특별한 원소가 하나 있어야 한다. 예를 들어 0을 포함하는 자연수 집합은 덧셈에 대해 모노이드를 이룬다.

// 문자열 모노이드: 이항 연산은 문자열 결합, 항등원은 빈 문자열
const concat = (a, b) => a + b;
const identity = "";

// 두 개의 문자열을 결합
let result = concat("Hello, ", "World!"); // "Hello, World!"
console.log(result);

// 항등원을 사용하여 결합
let resultWithIdentity = concat("Hello", identity); // "Hello"
console.log(resultWithIdentity);
  • 함수형 프로그래밍에서 모노이드는 데이터 결합을 다루는 데 유용하다. 예를 들어, 여러 개의 값을 결합하는 연산은 모노이드의 결합법칙과 항등원 성질을 활용하여 구현할 수 있다. 이 과정에서 불변성을 유지하면서 연산을 처리할 수 있다.

관점을 바꿔 이항연산을 적용한다는 것을 어떤 대상에서 다른 대상으로 "이동"하는 것으로 생각해보자. 어떤 수 n에 대해서 다시 n을 더하는 "Adder" 함수가 있다고 생각배호자. 이 Adder는 어떤 방식으로 합성될까? 5를 더하는 함수와 7을 더하는 함수를 합성하면 12를 더하는 함수가 될 것이다. 즉 Adder들 간의 합성은 덧셈 규칙을 그대로 유지한다고 볼 수 있다. 그렇다면 덧셈이라는 개념을 함수의 합성이라 생각해도 문제없을 것이다.


CHAPTER04. 크라이슬리 카테고리

// Writer 모나드를 간단히 표현하기 위한 구조
function writer(value, log) {
return { value, log };
}

// isEven 함수: 숫자가 짝수인지 확인하고 로그를 남김
function isEven(n) {
const result = n % 2 === 0;
return writer(result, `Checked if ${n} is even. `);
}

// negate 함수: 논리값을 반전시키고 로그를 남김
function negate(b) {
return writer(!b, `Negated the value. `);
}

// 두 함수를 합성: 숫자를 짝수인지 확인하고 결과를 반전
function isOdd(n) {
const p1 = isEven(n);
const p2 = negate(p1.value);
return writer(p2.value, p1.log + p2.log); // 값과 로그를 합침
}

// 사용 예시
const result = isOdd(4);
console.log(result.value); // false
console.log(result.log);   // "Checked if 4 is even. Negated the value. "
  • 순수함수를 유지하기 위해서는 메모이제이션이라는 문제가 있다. 전역없이 메모이제이션을 해가며 함수를 계속 호출해야하기 때문이다. 따라서 재귀함수가 FP 전반적으로 도입될 수 밖에 없다는 것을 깨달았다.
  • Writer 모나드와 Writer 카테고리는 함수형 프로그래밍에서 사이드 이펙트를 다루는 한 방법이다.
  • 특히, 순수 함수를 유지하면서도 부수적인 정보(예: 로그, 상태)를 처리할 수 있도록 도와준다.
  • Writer 개념을 통해 Exception 같은 참조투명성을 깨뜨리는 행위없이 순수함수를 통해 기타정보를 처리할 수 있다.

CHAPTER05. 곱과 합

// 곱(Product): 두 값을 결합하여 하나의 튜플로 나타냄
function product(a, b) {
  return [a, b]; // 튜플 형태로 두 값을 반환
}

const pair = product(3, "apple");
console.log(pair);         // [3, "apple"]
console.log(pair[0]);      // 3 (첫 번째 값)
console.log(pair[1]);      // "apple" (두 번째 값)
// 합(Coproduct): Either 패턴으로 Left와 Right 값을 처리
function Left(value) {
  return { type: 'Left', value };
}

function Right(value) {
  return { type: 'Right', value };
}

// 값을 선택하는 함수
function processEither(either) {
  if (either.type === 'Left') {
    return `Left value is: ${either.value}`;
  } else {
    return `Right value is: ${either.value}`;
  }
}

const leftValue = Left(42);
const rightValue = Right("Hello");

console.log(processEither(leftValue));  // Left value is: 42
console.log(processEither(rightValue)); // Right value is: Hello
  • 대상간 관계를 표현함에 있어 결합법칙을 만족해야한다는 조건때문에 곱과 합이 존재하게 된다. (사상과 관계의 차이: 일방향에 대한 조건)

  • 곱(Product) 은 두 대상을 조합하여 새로운 대상을 만들고, 그 대상에서 원래 두 대상으로 향하는 투영 함수(projections)가 존재하는 상황을 말한다. 따라서 곱의 의미는 두 값이 동시에 존재하는 상황을 나타낸다.

  • 합(Coproduct) 은 곱과 반대이다. 두 대상을 결합하는 것이지만, 이때는 두 대상에서 하나의 대상 C로 삽입 함수를 사용해 연결하는 방식이다.함수형 프로그래밍에서는 Either 타입이 합의 역할을 한다. (Either a b는 a 또는 b 중 하나를 선택하는 타입)
  • 카테고리 이론에서의 합(Coproduct) 은 합집합과 유사하지만, 추가적인 구조가 있다. 특히 서로소 유니온(Disjoint Union) 의 개념이 중요한데, 이는 서로 겹치지 않는 집합들을 합친다는 것을 의미한다.
  • 두 집합 a와 b가 있을 때, Coproduct에서는 단순히 원소들을 합치는 것 이상으로 각 원소가 어디서 왔는지 식별할 수 있는 구조를 제공한다.
  • 즉, A와 B의 원소가 어디서 왔는지 알 수 있도록 태그가 붙는 구조가 된다. 이를 프로그래밍에서는 Either 타입이나 Tagged Union이라고도 부른다.
  • 클래스 구조는 곱(Product)의 사례: 여러 속성을 동시에 포함하는 데이터 구조로, 모든 속성을 하나의 객체로 결합하여 관리하는 상황. 예를 들어 사용자 정보(이름, 이메일, 나이)를 객체로 묶어 처리할 때 사용된다.
  • 유니온(Union)과 이더(Either)는 합(Coproduct)의 사례: 여러 옵션 중 하나를 선택해서 사용하는 경우에 해당한다.
  • 결제 수단 선택에서 합(Coproduct) 개념 사용: 결제 시스템에서 신용카드, 페이팔, 계좌이체 등의 결제 방법 중 하나를 선택하는 상황은 합의 사례이다. 여러 옵션 중 하나만 선택되어야 하므로 합의 개념 적용
  • 에러 처리에서 합의 개념 적용: 프로그램에서 에러 처리는 성공과 실패 중 하나만 발생한다. 이때도 합의 개념을 사용하여 결과를 처리. 성공 또는 실패라는 두 가지 중 하나를 선택하는 구조.
  • 곱(Product)은 동시에 여러 정보를 포함: 예를 들어, 사용자 정보나 상품 정보를 다룰 때 각각의 속성들을 하나의 데이터 구조로 결합해야 한다. 이러한 상황에서는 곱의 개념을 사용하여 여러 속성을 하나의 객체로 처리.
  • iOS의 예로들자면 Result 타입에서 Result<Value, CustomError>는 곱과 합의 개념을 동시에 담고 있다.
  • Value의 경우 곱(Product): Value는 여러 속성들을 함께 포함한다. 예를 들어, 하나의 Value에 다양한 정보를 담고 있을 때, 이는 곱의 개념에 해당한다.
  • Result<Value, CustomError>는 합(Coproduct): Result는 성공(Value) 또는 실패(CustomError) 중 하나를 가진다. 즉, 두 값 중 하나만 선택될 수 있기 때문에 합의 개념을 반영.

CHAPTER06. 단순한 대수적 타입

곱 타입과 합 타입 모두 각각 데이터 구조를 정의할 때 유용하게 사용할 수 있지만, 실질적인 강점은 . 둘을 결합할 . 나타난다.

만약 (a * b) * c와 a * (b * c)를 모노이드인 곱 연산의 관점에서 바라본다면, 이 연산은 교환법칙을 만족하기 때문에 완벽히 동일(Equal)하다고 이야기할 수 있다.

하지만 곱 타입의 경우 각 튜플 안에 있는 요소들이 서로 정보의 손실 없이 매핑될 수 있는 일대일 대응함수가 존재하는 동형(Isomorphic)일 뿐이다. 즉, 두 곱 타입이 동형사상을 통해 서로 매핑이 가능하므로 “동형성에 의해 같다”고 말할 수는 있겠지만 엄밀한 의미에서 동일하지는 않다는 것이다.

// 데이터 구조의 결합 예시
const tuple1 = { person: { name: "Alice", age: 30 }, city: "New York" };
const tuple2 = { name: "Alice", address: { age: 30, city: "New York" } };

// 두 데이터를 변환하는 함수
function transformTuple1ToTuple2(t1) {
  return {
    name: t1.person.name,
    address: {
      age: t1.person.age,
      city: t1.city
    }
  };
}

function transformTuple2ToTuple1(t2) {
  return {
    person: {
      name: t2.name,
      age: t2.address.age
    },
    city: t2.address.city
  };
}

const transformed1 = transformTuple1ToTuple2(tuple1); // { name: 'Alice', address: { age: 30, city: 'New York' } }
const transformed2 = transformTuple2ToTuple1(tuple2); // { person: { name: 'Alice', age: 30 }, city: 'New York' }

console.log(transformed1); // { name: 'Alice', address: { age: 30, city: 'New York' } }
console.log(transformed2); // { person: { name: 'Alice', age: 30 }, city: 'New York' }
  • tuple1: 사람 정보가 person 객체 안에 있고, 그 사람이 사는 도시 정보는 따로 결합되어 있다.
  • tuple2: 이름과 주소가 결합되어 있으며, 주소 안에 나이와 도시 정보가 들어 있다.
  • 이 두 데이터 구조는 결합 순서만 다를 뿐, 본질적으로 동일한 데이터를 표현하고 있다.
  • 이를 변환하는 함수들을 만들어 어떤 결합 형태로 변형해도 같은 정보를 유지한다는 것이 곱 타입의 결합법칙이다.

const originalMap = { key1: "value1", key2: "value2" };

// 새로운 맵을 만들어서 기존 맵에 값을 추가하거나 수정
const updatedMap = { ...originalMap, key1: "newValue1", key3: "value3" };

console.log(originalMap);  // { key1: "value1", key2: "value2" } (원본은 그대로)
console.log(updatedMap);   // { key1: "newValue1", key2: "value2", key3: "value3" } (새 맵)
  • 맵(Map)이나 딕셔너리(Dictionary)를 수정할 때 기존 데이터를 직접 수정하지 않고 새로운 데이터 구조를 생성하는 방식을 사용해야한다.

const person = { name: "Alice", age: 30 };

// 패턴 매칭처럼 객체 분해 (destructuring)
const { name, age } = person;

console.log(name); // Alice
console.log(age);  // 30
  • FP에서는 데이터의 구조를 그대로 보존하면서 부분적으로 다시 분해해서 사용한다.
  • 불변 데이터를 분해하는 방법으로 대표적인 것이 패턴매치이다.

CHAPTER07. 펑터

  • "이와 같이 분해가 불가능하다는 제약은 미적분에서 볼 수 있는 연속성의 조건과 유사하다. 이런 의미에서 펑터는 “연속적”이라고 볼 수도 있다." 이부분이 정말 인상적이었다.
  • 펑터를 통해 모노이드를 선형적으로 표현할 수 있다는 것이 정말 신박한 발상이라고 느껴진 부분이었다. 함수형 프로그래밍은 이런 부분에서 정말 우아하다고 느껴진다.
  • map, foreach 고차함수들은 reader펑터들이라고 볼 수 있다. 컨텍스트를 유지, 외부 데이터를 캡쳐하면서 모노이드를 유지하기 때문이다.
  • 함수형 프로그래밍에서 함수를 계층적으로 설계한다고 보았을 때 reader펑터들은 최상위 계층의 함수들이라고 볼 수 있다.
  • 또한 이러한 Reader펑터들은 동일한 카테고리 안에서 계산이 이루어지면 엔도펑터, 다른 카테고리로 옮겨가면 사상이라고 볼 수 있다.
// 두 고차함수를 정의
const addOne = (x) => x + 1;  // 첫 번째 고차함수
const double = (x) => x * 2;  // 두 번째 고차함수

// 배열 펑터
const arrayFunctor = [1, 2, 3];

// 첫 번째 펑터에서 연산 적용 -> 두 번째 펑터로 결과 전달
const composedResult = arrayFunctor.map(addOne).map(double);

console.log(composedResult); // [4, 6, 8]
  • 펑터 합성(Functor Composition)은 우리가 평소에 하던 작업, 고차함수로 컨텍스트 가져온다음에 계산하고, 또다른 고차함수로 컨텍스트를 가져온담에 계산하고의 체이닝연산을 말하는 것이였다.

CHAPTER08. 펑터의 특성

// Pair라는 구조를 정의
class Pair {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }

  // bimap 함수는 두 개의 함수를 받아서 각각의 값에 적용
  bimap(f, g) {
    return new Pair(f(this.a), g(this.b));
  }
}

// Pair 인스턴스를 만들어서 테스트
const pair = new Pair(10, 20);

// 두 개의 값을 변환하는 함수
const newPair = pair.bimap(
  (x) => x * 2, // 첫 번째 값에 곱하기 2 적용
  (y) => y + 5  // 두 번째 값에 더하기 5 적용
);

console.log(newPair); // Pair { a: 20, b: 25 }
  • 이항펑터는 두개의 대상을 다른 카테고리로 매핑하는 펑터이다.
  • 이항 연산은 단순히 두 대상을 결합하는 것이 아니라, 두 대상을 결합할 때 그들 사이의 사상도 결합하도록 해야 하며, 이것이 이항 펑터의 역할이다. 즉 합이 아닌 곱의 개념이 들어가야한다.

// 공변펑터 (Covariant Functor)
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

// 반공변펑터 (Contravariant Functor)
const log = (message) => console.log(message);
const contramap = (fn, converter) => (input) => fn(converter(input));
const logWithPrefix = contramap(log, (input) => `Logging: ${input}`);
logWithPrefix('Hello'); // 'Logging: Hello'
  • 함수형 프로그래밍에서 재밌는 부분이 방향을 바꿀 수 있다는 점이다.
  • 공변펑터의 경우 하나의 함수가 값을 변환할 때 입력에서 출력으로 향하는 방향이 그대로 유지된다. 함수형 프로그래밍에서는 이를 값을 변환하는 것이라고 생각할 수 있다.
  • 반공변 펑터는 사상의 방향을 뒤집는 펑터이다. 함수형 프로그래밍에서는 입력 값을 변환하는 역할을 한다고 볼 수 있다.
  • 반공변 펑터가 이해하기 힘들었는데 알고보니 함수에 값을 넣기 전에, 그 값을 먼저 바꾼다는 개념이었다.
  • 쉽게 요약하면 입력값을 미리 처리한다음에 본래 함수에 넘기는 것이다.

const logNumber = (number) => console.log(number);
const logPrefixedNumber = contramap(logNumber, (str) => parseInt(str, 10));

// 이제 문자열로 전달된 숫자를 자동으로 변환한 후 로그에 출력
logPrefixedNumber('42'); // 42
  • 반공변 펑터가 정말 유용하게 쓰일 수 있는게 함수의 입력처리를 함수 본체와 분리를 할 수 있다는 점이다.
  • 만약 입력 값을 여러 가지 방식으로 변환하거나, 동일한 변환 과정을 여러 함수에 적용해야 한다면, contramap 같은 패턴은 코드의 재사용성을 높일 수 있다.