펑터 및 카테고리

작곡 소프트웨어

스모크 아트 큐브 스모크 — MattysFlicks — (CC BY 2.0)
참고 :이 글은 JavaScript ES6 +의 기능 프로그래밍 및 컴포지션 소프트웨어 기술을 처음부터 배우는“소프트웨어 구성”시리즈 (현재 책!)의 일부입니다. 계속 지켜봐 주시기 바랍니다. 앞으로 더 많은 것들이 있습니다!
<이전 | << 1 부에서 다시 시작 | 다음 >>

functor 데이터 형식은 매핑 할 수있는 것입니다. 내부의 값에 함수를 적용하는 데 사용할 수있는 인터페이스가있는 컨테이너입니다. functor가 보이면“mappable”이라고 생각해야합니다. Functor 유형은 일반적으로 구조를 유지하면서 입력에서 출력으로 매핑하는 .map () 메서드를 사용하여 객체로 표시됩니다. 실제로, "구조 보존"은 반환 값이 동일한 유형의 functor임을 의미합니다 (컨테이너 내부의 값은 다른 유형일 수 있음).

펑 터는 내부에 0 개 이상의 물건이 들어있는 상자와 매핑 인터페이스를 제공합니다. 배열은 functor의 좋은 예이지만 약속, 스트림, 나무, 객체 등을 포함하여 다른 많은 종류의 객체를 매핑 할 수도 있습니다. JavaScript의 내장 배열 및 promise 객체는 functors처럼 작동합니다. 컬렉션 (배열, 스트림 등)의 경우 .map ()은 일반적으로 컬렉션을 반복하고 지정된 함수를 컬렉션의 각 값에 적용하지만 모든 함수가 반복되는 것은 아닙니다. 펑 터는 실제로 특정 상황에서 함수를 적용하는 것입니다.

약속은 .map () 대신 .then () 이름을 사용합니다. 중첩 된 약속이있는 경우를 제외하고는 일반적으로 .then ()을 비동기 .map () 메소드로 생각할 수 있습니다.이 경우 외부 약속을 자동으로 랩 해제합니다. 다시 말하지만 약속이 아닌 값의 경우 .then ()은 비동기 .map ()처럼 작동합니다. 약속 한 값의 경우 .then ()은 모나드의 .flatMap () 메서드와 같은 역할을합니다 (때로는 .chain ()이라고도 함). 따라서 약속은 꽤 펑터가 아니며 모나드가 아니지만 실제로는 일반적으로 약속으로 사용할 수 있습니다. 아직 모나드에 대해 걱정하지 마십시오. 모나드는 일종의 펑터이므로 펑터를 먼저 배워야합니다.

다양한 다른 것들도 펑터로 만들 수있는 많은 라이브러리가 존재합니다.

Haskell에서 functor 유형은 다음과 같이 정의됩니다.

fmap :: (a-> b)-> f a-> f b

a를 가져 와서 b와 그 내부에 0 이상이있는 functor를 반환하는 함수가 주어진 경우 : fmap은 내부에 b가 0 이상인 상자를 반환합니다. f a 및 f b 비트는 "a의 functor"및 "b의 functor"로 읽을 수 있습니다. 즉, f a는 상자 내부에 있고 f b는 상자 내부에 bs가 있습니다.

functor를 사용하는 것은 쉽습니다. map ()을 호출하면됩니다.

const f = [1, 2, 3];
f.map (더블); // [2, 4, 6]

펑터 법칙

카테고리에는 두 가지 중요한 특성이 있습니다.

  1. 정체
  2. 구성

펑 터는 범주 간 매핑이므로 펑 터는 정체성과 구성을 존중해야합니다. 함께, 그들은 functor 법률로 알려져 있습니다.

정체

항등 함수 (x => x)를 f.map ()에 전달하면 f는 functor입니다. 결과는 f와 같은 의미 여야합니다.

const f = [1, 2, 3];
f.map (x => x); // [1, 2, 3]

구성

펑 터는 합성법을 준수해야합니다. F.map (x => f (g (x)))는 F.map (g) .map (f)와 같습니다.

함수 구성은 하나의 함수를 다른 함수의 결과에 적용하는 것입니다. 예를 들어, x와 함수 f와 g가 주어지면, 조성 (f ∘ g) (x) (일반적으로 f ∘ g로 단축 됨-(x)는 암시)는 f (g (x))를 의미합니다.

많은 기능적 프로그래밍 용어는 범주 이론에서 비롯되며 범주 이론의 본질은 구성입니다. 카테고리 이론은 처음에는 무섭지 만 쉽지 않습니다. 다이빙 보드에서 뛰어 내리거나 롤러 코스터를 타는 것처럼. 다음은 몇 가지 핵심 요점에서 범주 이론의 기초입니다.

  • 범주는 개체 간 개체와 화살표의 모음입니다 ( "개체"는 문자 그대로 무엇이든 의미 할 수 있음).
  • 화살표는 형태로 알려져 있습니다. 형태론은 코드에서 함수로 생각되고 표현 될 수 있습니다.
  • 연결된 개체 그룹 a-> b-> c의 경우 a-> c에서 직접 구성이 있어야합니다.
  • 모든 화살표는 컴포지션으로 표시 될 수 있습니다 (물체의 정체성 화살표가있는 컴포지션 일지라도). 범주의 모든 개체에는 식별 화살표가 있습니다.

a를 취하고 b를 반환하는 함수 g와 b를 취하고 c를 반환하는 다른 함수 f가 있다고 가정 해보십시오. f와 g의 구성을 나타내는 함수 h도 있어야합니다. 따라서, a-> c의 조성은 조성 f ° g (g 이후의 f)입니다. 따라서 h (x) = f (g (x))입니다. 함수 구성은 왼쪽에서 오른쪽이 아니라 오른쪽에서 왼쪽으로 작동하므로 f ∘ g를 g 다음에 f라고하는 경우가 많습니다.

구성은 연관 적입니다. 기본적으로 이는 여러 기능을 구성 할 때 (멋진 느낌이라면 형태) 괄호가 필요하지 않음을 의미합니다.

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

JavaScript에서 작성법을 다시 살펴 보겠습니다.

functor가 주어지면 F :

const F = [1, 2, 3];

다음은 동일합니다.

F.map (x => f (g (x)));
//와 같습니다.
F.map (g) .map (f);

Endofunctors

endofunctor는 범주에서 동일한 범주로 다시 매핑되는 functor입니다.

functor는 카테고리에서 카테고리로 맵핑 할 수 있습니다. X-> Y

endofunctor는 카테고리에서 동일한 카테고리로 맵핑됩니다. X-> X

모나드는 endofunctor입니다. 생각해 내다:

“모나드는 endofunctors 범주의 단일체 일뿐입니다. 뭐가 문제 야?"

그 인용이 조금 더 이해되기 시작하기를 바랍니다. 나중에 모노 이드와 모나드를 보게됩니다.

자신 만의 Functor 구축

functor의 간단한 예는 다음과 같습니다.

const 신원 = 값 => ({
  지도 : fn => Identity (fn (value))
});

보시다시피 functor 법률을 충족합니다.

// trace ()는 쉽게 검사 할 수있는 유틸리티입니다
// 컨텐츠.
const 추적 = x => {
  console.log (x);
  x를 반환;
};
const u = 신원 (2);
// 신원 법
u.map (추적); // 2
u.map (x => x) .map (trace); // 2
const f = n => n + 1;
const g = n => n * 2;
// 작문법
const r1 = u.map (x => f (g (x)));
const r2 = u.map (g) .map (f);
r1.map (추적); // 5
r2.map (추적); // 5

이제 배열을 통해 매핑 할 수있는 것처럼 모든 데이터 유형을 통해 매핑 할 수 있습니다. 좋은!

functor가 JavaScript에서 얻을 수있는 것처럼 간단하지만 JavaScript의 데이터 유형에서 예상되는 일부 기능이 누락되었습니다. 그것들을 추가합시다. + 연산자가 숫자와 문자열 값을 처리 할 수 ​​있다면 멋지지 않습니까?

이 작업을 수행하려면 .valueOf ()를 구현하기 만하면됩니다.이 기능은 functor에서 값을 풀기위한 편리한 방법 인 것 같습니다.

const 신원 = 값 => ({
  지도 : fn => Identity (fn (value)),
  valueOf : () => 값,
});
const ints = (아이덴티티 (2) + 아이덴티티 (4));
자취 (ints); // 6
const hi = (Identity ( 'h') + Identity ( 'i'));
미량 (hi); // "안녕하세요"

좋은. 그러나 콘솔에서 Identity 인스턴스를 검사하려면 어떻게해야합니까? "Identity (value)"라고 말하면 좋을 것입니다. .toString () 메소드를 추가해 봅시다 :

toString : () =>`ID ($ {value})`,

시원한. 표준 JS 반복 프로토콜도 활성화해야합니다. 커스텀 반복자를 추가하면됩니다 :

[Symbol.iterator] : 함수 * () {
  수율 값;
}

이제 이것은 작동합니다 :

// [Symbol.iterator]는 표준 JS 반복을 활성화합니다.
const arr = [6, 7, ... ID (8)];
미량 (arr); // [6, 7, 8]

Identity (n)을 가져 와서 n + 1, n + 2 등을 포함하는 ID 배열을 반환하려면 어떻게해야합니까? 쉬워요?

const fRange = (
  스타트,
  종료
) => Array.from (
  {길이 : 끝-시작 + 1},
  (x, i) => 신분 (i + start)
);

아, 그러나 이것이 functor와 함께 작동하려면 어떻게해야합니까? 데이터 유형의 각 인스턴스가 해당 생성자에 대한 참조를 가져야한다는 스펙이있는 경우 어떻게해야합니까? 그럼 당신은 이것을 할 수 있습니다 :

const fRange = (
  스타트,
  종료
) => Array.from (
  {길이 : 끝-시작 + 1},
  
  //`Identity`를`start.constructor`로 변경
  (x, i) => start.constructor (i + start)
);
const 범위 = fRange (Identity (2), 4);
range.map (x => x.map (trace)); // 2, 3, 4

값이 functor인지 확인하기 위해 테스트하려면 어떻게해야합니까? 확인하기 위해 Identity에 정적 메소드를 추가 할 수 있습니다. 우리는 정적 .toString ()을 던져야합니다.

Object.assign (ID, {
  toString : () => '아이덴티티',
  x => typeof x.map === '함수'
});

이 모든 것을 함께합시다 :

const 신원 = 값 => ({
  지도 : fn => Identity (fn (value)),
  valueOf : () => 값,
  toString : () =>`ID ($ {value})`,
  [Symbol.iterator] : 함수 * () {
    수율 값;
  },
  생성자 : 신원
});
Object.assign (ID, {
  toString : () => '아이덴티티',
  x => typeof x.map === '함수'
});

functor 또는 endofunctor 자격을 갖추기 위해 추가 자료가 필요하지 않습니다. 엄밀히 말하면 편리합니다. functor에 필요한 것은 functor 법칙을 만족시키는 .map () 인터페이스입니다.

왜 펑터인가?

펑 터는 여러 가지 이유로 훌륭합니다. 가장 중요한 것은 모든 데이터 유형에서 작동하는 방식으로 많은 유용한 것들을 구현하는 데 사용할 수있는 추상화입니다. 예를 들어, 일련의 작업을 시작하지만 functor 내부의 값이 정의되지 않았거나 null이 아닌 경우에만 어떻게해야합니까?

// 술어 만들기
const가 존재합니다 = x => (x.valueOf ()! == undefined && x.valueOf ()! == null);
const ifExists = x => ({
  맵 : fn => 존재 (x)? x.map (fn) : x
});
const add1 = n => n + 1;
const double = n => n * 2;
// 아무 반응이 없습니다...
ifExists (Identity (undefined)). map (trace);
// 여전히 아무것도 ...
ifExists (Identity (null)). map (trace);
// 42
ifExists (ID (20))
  .map (add1)
  .map (더블)
  .map (추적)
;

물론 함수형 프로그래밍은 작은 함수를 작성하여 더 높은 수준의 추상화를 생성하는 것입니다. functor와 작동하는 일반 맵을 원한다면 어떻게해야합니까? 그렇게하면 인수를 부분적으로 적용하여 새 함수를 만들 수 있습니다.

쉬운. 좋아하는 자동 카레를 선택하거나이 마법 주문을 사용하십시오.

const 카레 = (
  f, arr = []
) => (... args) => (
  a => a.length === f.length?
    f (... a) :
    카레 (f, a)
) ([... arr, ... args]);

이제 맵을 커스터마이징 할 수 있습니다 :

const map = curry ((fn, F) => F.map (fn));
const double = n => n * 2;
const mdouble = 맵 (더블);
mdouble (아이덴티티 (4)). map (trace); // 8

결론

펑 터는 우리가 매핑 할 수있는 것들입니다. 보다 구체적으로, functor는 범주 간 매핑입니다. functor는 카테고리에서 동일한 카테고리 (예 : endofunctor)로 다시 매핑 할 수도 있습니다.

범주는 개체 사이에 화살표가있는 개체의 모음입니다. 화살표는 형태 (일명 기능, 일명 작곡)를 나타냅니다. 카테고리의 각 객체에는 항등 형태 (x => x)가 있습니다. 객체 A-> B-> C의 모든 체인에 대해 컴포지션 A-> C가 있어야합니다.

펑 터는 모든 고차원 추상화로, 모든 데이터 유형에 적합한 다양한 일반 함수를 만들 수 있습니다.

다음 : 기능성 믹스 인>

라이브 1 : 1 멘토링으로 기술 수준을 높이십시오

DevAnywhere는 고급 JavaScript 기술 수준을 향상시키는 가장 빠른 방법입니다.

  • 라이브 레슨
  • 유연한 시간
  • 1 : 1 멘토링
  • 실제 프로덕션 앱 구축
https://devanywhere.io/

Eric Elliott는“JavaScript 응용 프로그램 프로그래밍”(O'Reilly)의 저자이며 DevAnywhere.io의 공동 설립자입니다. 그는 Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC 및 Usher, Frank Ocean, Metallica 등을 포함한 최고의 레코딩 아티스트를위한 소프트웨어 경험에 기여했습니다.

그는 세계에서 가장 아름다운 여성과 함께 원하는 곳 어디에서나 일합니다.