Chap11 모나드
11-1) 모나드 이해하기
- 모나드(Monad)는 ‘카테고리 이론(category theory)’ 분야에서 사용되는 용어로, 프로그래밍에서 모나드는 ‘코드 설계 패턴(design pattern)’으로서 몇 개의 인터페이스를 구현한 클래스다.
(1) 타입 클래스란?
- 2차 고차 함수 callMap은 두 번째 고차 매개변수 b가 map이라는 메서드를 가졌다고 가정함
const callMap = fn => b => b.map(fn)
- 제대로 된 코드를 사용하지 않으면 프로그램은 제대로 인식을 하지 못해 비정상적으로 종료되므로, 매개변수 b가 반드시 map 메서드가 있는 타입이라고 제한을 해야함
const callMap = <T, U>(fn: (T) => U) => <T extends {map(fn)}>(b: T) => b.map(fn)
이를 통해 비정상으로 종료되는 것을 막을 수 있음
- 하스켈(haskell) 언어는 객체지향 언어와 달리 모나드 방식 설계를 통해 map과 of가 있는 Monad<T> 클래스를 만듦
class Monad<T> {
constructor(public value: T) {}
static of<U>(value: U): Monad<U> {return new Monad<U>(value)}
map<U>(fn: (x: T) => U): Monad<U> {return new Monad<U>(fn(this.value))}
}
- 위와 같은 Monad<T> 클래스를 타입 클래스라고 하며, 타입 클래스는 함수를 만들 때 특별한 타입으로 제약하지 않아도 됨.
const callMonad = (fn) => (b) => Monad.of(b).map(fn).value
- 위와 같이 모나드 클래스를 구현했을 때 장점은 타입에 따른 안정성 뿐만 아니라 코드 재사용성(code reusability)가 뛰어난 범용 함수를 쉽게 만들 수 있다는 점이다.
(2) 고차 타입이란?
- Monad<T>처럼 타입 T를 한 단계 높은 타입으로 변환하는 용도의 타입을 ‘고차 타입(higher-kinded type)’이라고 한다. 하지만 타입스크립트는 스칼라처럼 3차 이상의 고차 타입을 만들 순 없다.
(3) 카테고리 이론이란?
- 카테고리 이론(category theory)는 1940년대에 시작된 수학의 한 분야로, 함수형 프로그래밍 언어의 중요한 이론적 배경이 되었다.
- 수학에서 집합(set)은 프로그래밍에서의 타입으로, 수학에서 카테고리는 ‘집합의 집합’이기 때문에 프로그래밍에서 카테고리는 고차 타입이라는 개념으로 이해할 수 있다.
(4) 판타지랜드 규격
- 모나드는 카테고리 이론에서 사용되는 용어였지만, 하스켈 언어의 Prelude라는 표준 라이브러리에서 사용되는 용어이기도 하다.
- 모나드는 ‘모나드 룰(Monad raw)’이라고 하는 코드 설계 원칙에 맞춰 구현된 클래스를 의미하며, 다음과 같은 네가지 요소의 조건을 만족하면 그 클래스는 모나드라고 할 수 있다.
(https://github.com/fantasyland/fantasy-land) – 모나드 계층 구조 참조
※ 모나드 네가지 요소
• 펑터(Functor) : map이라는 인스턴스 메서드를 가지는 클래스
• 어플라이(Apply) : 펑터이면서 ap라는 인스턴스 메서드를 가지는 클래스
• 애플리커티브(Applicative) : 어플라이이면서 of라는 클래스 메서드를 가지는 클래스
• 체인(Chain) : 애플리커티브이면서 chain이라는 메서드를 가지는 클래스
(5) 모나드 룰
- 어떤 클래스 이름이 M이고 이 클래스의 인스턴스를 m이라고 할 때 모나드는 앞서 언급한 애플리커티브와 체인의 기능을 가지고 있으며 다음처럼 왼쪽과 오른쪽 법칙을 만족하게 구현한다.
왼쪽 법칙(Left identity) : M.of(a).chain(f) == f(a)
오른쪽 법칙(right identity) : m.chain(M.of) == m
11-2) Identity 모나드 이해와 구현
(1) 값 컨테이너 구현용 IValuable<T> 인터페이스 구현
- 일반적으로 컨테이너는 배열을 의미하지만, 배열이 아닌 한 개의 값만 가지는 컨테이너 클래스를 생각해 볼 수 있다. 이 같은 컨테이너 클래스는 구체적인 타입이아닌 모든 타입 T를 가지는 컨테이너로 ‘값 컨테이너(value container)’라고 한다.
※ 값 컨테이너 예
export interface IValuable<T> {
value() : T
}
(2) 클래스 이름이 왜 Identity인가?
- 함수형 프로그래밍에서 identity는 다음과 같은 의미를 갖는 함수
const identity = <T>(value: T): T => value
- Identity는 앞서 본 map, ap, of, chain과 같은 기본 메서드만 구현한 모나드로 카테고리 이론에서 자신의 타입에서 다른 타입으로 갔다가 돌아올 때 값이 변경되지 않는 카테고리를 Identity라고 부른다. ㄷ\
(3) 값 컨테이너로서의 Identity<T> 구현하기
※ _value를 private하게 구현하고, value 메서드를 public 하게 구현한 예
import {IValuable} from '../interfaces'
export class Identity<T> implements IValuable<T> {
constructor(private _value: T) {}
value() {return this._value}
}
(4) Isetoid<T> 인터페이스와 구현
- 판타지랜드 규격에서 setoid는 equals라는 이름의 메서드를 제공하는 인터페이스를 의미하며, 다음과 같이 구현이 가능
import {IValuable} from './IValuable'
export interface ISetoid<T> extends IValuable<T> {
equals<U>(value: U): boolean
}
※ Identity에 Isetoid를 구현한 예
import {ISetoid} from '../interfaces'
export class Identity<T> implements ISetoid<T> {
constructor(private _value: T) {}
value() {return this._value}
equals<U>(that: U): boolean {
if(that instanceof Identity)
return this.value() == that.value()
return false
}
}
(5) Ifunctor<T> 인터페이스와 구현
- 판타지랜드 규격에서 펑터(functor)는 map이라는 메서드를 제공하는 인터페이스로, ‘엔도펑터(endofunctor)’라는 특별한 성질을 만족시켜야 한다.
export interface IFunctor<T> {
map<U>(fn: (x: T) => U)
}
(6) 엔도펑터란?
- ‘엔도(endo)’는 단어 앞에 붙는 접두사로, 엔도펑터(endofunctor)는 특정 카테고리에서 출발해도 도착 카테고리는 다시 출발 카테고리가 되게 하는 펑터를 의미한다.
(7) Iapply<T> 인터페이스와 구현
- 어플라이(apply)는 자신은 펑터이면서 동시에 ap라는 메서드를 제공하는 인터페이스이다.
import {IFunctor} from './IFunctor'
export interface IApply<T> extends IFunctor<T> {
ap<U>(b: U)
}
(8) Iapplicative<T> 인터페이스와 구현
- 애플리커티브(applicative)는 그 자신이 어플라이이면서 of 라는 클래스메서드(정적 메서드)를 추가로 제공하는 인터페이스다.
import {IApply} from './IApply'
export interface IApplicative<T> extends IApply<T> {
// static of(value: T)
}
(9) Ichain<T> 인터페이스와 구현
- 체인(chian)은 그 자신이 어플라이이면서 chian이라는 메서드를 구현하는 인터페이스이다.
import {IApply} from './IApply'
export interface IChain<T> extends IApply<T> {
chain<U>(fn: (T) => U)
}
- 체인의 chain 메서드는 펑터의 map과 달리 엔도펑터로 구현해야할 의무가 없으므로 시그너처는 같지만 구현 내용은 조금 다른데, 엔도펑터인 map은 항상 같은 카테고리에 머무르지만, chian은 자신이 머무르고 싶은 카테고리를 스스로 정해야한다.
※ map과 chain의 메소드 비교 (결괏값은 동일)
import {Identity} from '../classes/Identity'
console.log(
Identity.of(1).map(value => `the count is ${value}`).value(),
Identity.of(1).chain(value => Identity.of(`the count is ${value}`)).value()
)
(10) IMonad<T> 인터페이스와 구현
import {IChain} from './IChain'
import {IApplicative} from './IApplicative'
export interface IMonad<T> extends IChain<T>, IApplicative<T> {}
※ 최종적으로 구현된 모나드 클래스 예
import {ISetoid, IMonad} from '../interfaces'
export class Identity<T> implements ISetoid<T>, IMonad<T> {
constructor(private _value: T) {}
// IValuable
value() {return this._value}
//Iapplicative
static of<T>(value: T): Identity<T> {return new Identity<T>(value)}
// ISetoid
equals<U>(that: U): boolean {
if(that instanceof Identity)
return this.value() == that.value()
return false
}
// IFunctor
map<U, V>(fn: (x: T) => U): Identity<U> {
return new Identity<U>(fn(this.value()))
}
// IApply
ap<U>(b: U) {
const f = this.value()
if(f instanceof Function)
return Identity.of<U>((f as Function)(b))
}
// IChain
chain<U>(fn: (T) => U): U {
return fn(this.value())
}
}
※ 왼쪽 법칙 테스트 예
import {Identity} from '../classes/Identity'
const a = 1
const f = a => a * 2
console.log(
Identity.of(a).chain(f) == f(a) // true
)
※ 오른쪽 법칙 테스트 예
import {Identity} from '../classes/Identity'
const m = Identity.of(1)
console.log(
m.chain(Identity.of).equals(m)
)
'TypeScript' 카테고리의 다른 글
Do it! 타입스크립트 프로그래밍 10장 (0) | 2020.05.21 |
---|---|
Do it! 타입스크립트 프로그래밍 9장 (0) | 2020.05.20 |
Do it! 타입스크립트 프로그래밍 8장 (0) | 2020.05.13 |
Do it! 타입스크립트 프로그래밍 7장 (0) | 2020.05.07 |
Do it! 타입스크립트 프로그래밍 6장 (0) | 2020.05.06 |