Million Dreams
100만개의 꿈을 꾸는 개발자 지망생
Do it! 타입스크립트 프로그래밍 11장

Chap11 모나드

11-1) 모나드 이해하기

- 모나드(Monad)카테고리 이론(category theory)’ 분야에서 사용되는 용어로, 프로그래밍에서 모나드는 코드 설계 패턴(design pattern)’으로서 몇 개의 인터페이스를 구현한 클래스다.

 

(1) 타입 클래스란?

- 2차 고차 함수 callMap은 두 번째 고차 매개변수 bmap이라는 메서드를 가졌다고 가정함

const callMap = fn => b => b.map(fn)

 

- 제대로 된 코드를 사용하지 않으면 프로그램은 제대로 인식을 하지 못해 비정상적으로 종료되므로, 매개변수 b가 반드시 map 메서드가 있는 타입이라고 제한을 해야함

const callMap = <TU>(fn: (T) => U) => <T extends {map(fn)}>(b: T) => b.map(fn)

이를 통해 비정상으로 종료되는 것을 막을 수 있음

 

- 하스켈(haskell) 언어는 객체지향 언어와 달리 모나드 방식 설계를 통해 mapof가 있는 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> 구현하기

_valueprivate하게 구현하고, value 메서드를 public 하게 구현한 예

import {IValuablefrom '../interfaces'

 

export class Identity<Timplements IValuable<T> {

    constructor(private _value: T) {}

    value() {return this._value}

}

 

(4) Isetoid<T> 인터페이스와 구현

- 판타지랜드 규격에서 setoidequals라는 이름의 메서드를 제공하는 인터페이스를 의미하며, 다음과 같이 구현이 가능

import {IValuablefrom './IValuable'

 

export interface ISetoid<Textends IValuable<T> {

    equals<U>(value: U): boolean

}

 

IdentityIsetoid를 구현한 예

import {ISetoidfrom '../interfaces'

 

export class Identity<Timplements 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 {IFunctorfrom './IFunctor'

 

export interface IApply<Textends IFunctor<T> {

    ap<U>(b: U)

}

 

(8) Iapplicative<T> 인터페이스와 구현

- 애플리커티브(applicative)는 그 자신이 어플라이이면서 of 라는 클래스메서드(정적 메서드)를 추가로 제공하는 인터페이스다.

import {IApplyfrom './IApply'

 

export interface IApplicative<Textends IApply<T> {

    // static of(value: T)

}

 

(9) Ichain<T> 인터페이스와 구현

- 체인(chian)은 그 자신이 어플라이이면서 chian이라는 메서드를 구현하는 인터페이스이다.

 

import {IApplyfrom './IApply'

 

export interface IChain<Textends IApply<T> {

    chain<U>(fn: (T) => U)

}

 

- 체인의 chain 메서드는 펑터의 map과 달리 엔도펑터로 구현해야할 의무가 없으므로 시그너처는 같지만 구현 내용은 조금 다른데, 엔도펑터인 map은 항상 같은 카테고리에 머무르지만, chian은 자신이 머무르고 싶은 카테고리를 스스로 정해야한다.

 

mapchain의 메소드 비교 (결괏값은 동일)

import {Identityfrom '../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 {IChainfrom './IChain'

import {IApplicativefrom './IApplicative'

 

export interface IMonad<Textends IChain<T>, IApplicative<T> {}

 

 

최종적으로 구현된 모나드 클래스 예

import {ISetoidIMonadfrom '../interfaces'

 

export class Identity<Timplements 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<UV>(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 {Identityfrom '../classes/Identity'

 

const a = 1

const f = a => a * 2

console.log(

    Identity.of(a).chain(f== f(a// true

)

 

오른쪽 법칙 테스트 예

import {Identityfrom '../classes/Identity'

 

const m = Identity.of(1)

 

console.log(

    m.chain(Identity.of).equals(m)

)

 

  Comments,     Trackbacks