타입스크립트 기초 - 22

Stulta Amiko·2022년 8월 16일
0

타입스크립트 기초

목록 보기
22/24
post-thumbnail

모나드

모나드는 수학의 카테고리 이론이라는 분야에서 사용되는 용어이다.
프로그래밍에서의 모나드는 일종의 코드 설계 패턴으로서 몇 개의 인터페이스를 구현한 클래스이다.
모나드 클래스는 몇가지 공통적인 특징이 있다.

타입 클래스

모나드를 이해하기 위해서는 타입클래스가 왜 필요한지 알아야한다.
다음 2차 고차함수 callMap은 두번째 고차 매개변수 b가 map 이라는 메서드를 가졌다는 가정으로 구현되어 있다.

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

다음코드를 사용하면 작성자의 의도대로 작동한다.

callMap(a => a+1)([1])

하지만 다음과 같이 작성하게 된다면 문제가 발생하게 된다.

callMap(a => a+1)(1)

비정상 종료가 된다.
이를 방지하기 위해서 매개변수 b는 반드시 map 메서드가 있는 타입이라고 타입을 제한해야한다.

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

이런식으로 하면 비정상적인 종료를 방지하기위해 작성시점에서 오류를 띄우게 된다.

하스켈에서는 발상의 전환을 해서 모나드식 설계를 이루어 냈다.
보통의 객체지향언어라면 Number클래스를 만들고 map이라는 메서드를 구현하겠지만 그런방식이 아닌 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))
}

위와같은 모나드 클래스를 두고 타입클래스 라고한다.
타입 클래스는 다음처럼 함수를 만들 때 특별한 타입으로 제약하지 않아도된다.

const callMonad = (fn) => (b) => Monad.of(b).map(fn).value

Monad<T>와 같은 타입클래스덕분에 타입에 대한 안정성을 보장하면서도 코드의 재사용성이 뛰어난 범용함수를 쉽게 만들 수 있다.
callMonad와 같은 함수는 한 번 만들어두면 다음처럼 매개변수의 타입에 무관한 간결한 코드를 쉽게 작성할 수 있다.

callMonad((a: number) => a+1)(1)
callMonad((a: number[]) => a.map(value => value+1))([1,2,3,4])

고차타입

앞에서 본 Monad<T>는 타입 T를 Monad<T> 타입으로 변환했다가 다시 T 타입으로 변환해 준다. Monad<T> 처럼 타입 T를 한단계 더 높은 타입으로 변환하는 용도의 타입을 고차타입이라고 한다.

판타지랜드 규격

위와 같은 구조를 모나드 계층 구조라고 한다.
판타지랜드 규격은 하스켈 표준 라이브러리 구조를 자바스크립트 방식으로 재구성 한 것을 뜻한다.

어떤 클래스가 다음 조건을 만족하면 그 클래스는 모나드이다.
펑터,어플라이,애플리커티브,체인 을 가지고있으면 모나드이다.

펑터는 map이라는 인스턴스 메서드를 가지는 것을 의미하고
어플라이는 펑터이면서 ap 메서드를 가지는 것을 의미하고
애플리커티브는 어플라이이면서 of라는 클래스 메서드를 가지는것을 의미하고
체인은 어플라이이면서 chain 메서드를 가지는 것을 의미한다.

위 조건을 모두 만족하면 모나드라는 것이다.

identity 모나드 이해와 구현

값 컨테이너 구현용 IValueable<T> 인터페이스 구현

어떤 타입 T가 있을 때 배열 T[]는 같은 타입의 아이템을 여러개 가지고있는 컨테이너이다.
앞에서 본 Monad<T> 처럼 배열이 아닌 단지 한개의 값만 가지는 컨테이너 클래스를 생각해 볼 수 있다.
이 컨테이너 클래스는 number 와 같은 구체적인 타입의 값을 가지는 것이 아니라 모든 타입 T를 가질 수 있는 제네릭 컨테이너 클래스를 생각할 수 있다.
이처럼 타입 T를 가지는 값의 컨테이너를 값 컨테이너라고 한다.

export interface IValuable<T>{
    value(): T
}

identity 인 이유

함수형 프로그래밍에서 identity는 항상 다음처럼 구현하는 특별한 의미의 함수이다.

const identity = <T>(value: T): T => value

앞에서 살펴본 샘플코드가 있는데
R.ifElse의 false 조건 부분에 사용된 R.identity가 앞의 identity를 구현하고 있다.

const applyDiscount = (minimum: number,discount: number) => R.ifElse(
	R.flip(R.gte)(minimum),
    R.flip(R.subtract)(discount),
    R.identity
    )

Identity는 앞에서 본 map,ap,of,chain과 같은 기본 메서드만 구현한 모나드이다.
카테고리 이론에서 자신의 타입에서 다른타입으로 갔다가 돌아올 때 값이 변경되지 않는 카테고리를 Identity라고 부른다.

다음 코드에서 Identity<number> 타입은 chain 메서드를 통해 다시 자기 자신의 타입으로 돌아올 수 있는데 이런 의미에서 Identity라는 이름으로 짓게 되는것이다.

값 컨테이너로서의 Identity<T> 구현하기

앞에서 본 Monad<T>는 코드 분량을 줄이고자 value 속성을 public 하게 사용했는데 Identity는 _value 속성을 private 하게 구현하는 대신 value를 public 하게 구현한다.

import { IValuable } from "../interface/IValuable";

export class Identity<T> implements IValuable<T>{
    constructor(private _value: T){}
    value() {return this._value}
}

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

판타지랜드 규격에서 setoid는 equals 라는 이름의 메서드를 제공하는 인터페이슬 의미한다.

import { IValuable } from "./IValuable";

export interface ISetoid<T> extends IValuable<T>{
    equals<U>(value: U): boolean
}

ISetoid 인터페이스는 위와같이 구현한다.

import { ISetoid } from "../interface/ISetoid";

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
    }
}

그리고 Identity에 ISetoid는 위와같이 구현한다.

그러면 ISetoid를 구현했으니 테스트를 할수있는 테스트 코드를 짜보자

import { Identity } from "../classes/Identity";

const one = new Identity(1), anotherOne = new Identity(1)
const two = new Identity(2)

console.log(
    one.equals(anotherOne),
    one.equals(two),
    one.equals(1),
    one.equals(null),
    one.equals([1])
)

실행결과
true false false false false

IFunctor<T> 인터페이스 구현

판타지랜드 규격에서 펑터는 map이라는 메서드를 제공하는 인터페이스이다.
카테고리 이론에서 펑터는 엔도펑터라는 성질을 만족시켜야한다.

export interface IFunctor<T>{
    map<U>(fn: (x:T) => U)
}

엔도펑터란

엔도는 일종의 접두사이다 특정 카테고리에서 출발해도 도착 카테고리는 다시 출발 카테고리가 되게 하는 펑터를 의미한다.
다음 Identity<T>의 map 메서드는 엔도펑터로 동작하게 만든 코드이다.
타입이 중간에 바뀔 수는 있지만 카테고리는 여전히 Identity에 머문다.

import { IFunctor,ISetoid } from "../interface";

export class Identity<T> implements ISetoid<T>,IFunctor<T>{
    constructor(private _value:T){}
    //IValuable
    value() {return this._value}
    //ISetoid
    equals<U>(that: U): boolean {
        if(that instanceof Identity)
            return this.value() == that.value()
        return false
    }
    //IFunctor
    map<U>(fn: (x:T) => U){
        return new Identity<U>(fn(this.value()))
    }
}

IApply<T> 인터페이스 구현

판타지랜드 규격에서 어플라이는 자신은 펑터이면서 동시에 ap 메서드를 제공하는 인터페이스 이다.

import { IFunctor } from "./IFunctor";

export interface IApply<T> extends IFunctor<T>{
    ap<U>(b: U)
}

IApply를 구현하는 컨테이너는 값 컨테이너로서 뿐만 아니라 고차함수의 컨테이너로도 작동한다.

import { Identity } from "../classes/Identity";

const add = x => y => x+y
const id = new Identity(add)

console.log(
    id.ap(1).ap(2).value()
)

위는 IApply를 시험하는 코드이다.

Identity에는 다음과같이 작성한다.

ap<U>(b: U){
        const f = this.value()
        if(f instanceof Function)
            return Identity.of<U>((f as Function)(b))
    }

아직 of메서드를 작성하지 않아서 오류가 나는데 이는 다음 인터페이스를 구현하면 오류가 해결된다.

IApplicative<T> 인터페이스 구현

IApplicative 는 자신이 어플라이면서 of메서드를 추가 제공하는 인터페이스이다.

import { IApply } from "./IApply";

export interface IApplicative<T> extends IApply<T>{
    
}
    //IApplicative
    static of<T>(value: T): Identity<T> {return new Identity<T>(value)}

Identity에는 위와같이 작성한다.

IChain<T> 인터페이스 구현

체인은 자신이 어플라이이면서 chain 메서드를 구현하는 인터페이스를 뜻한다.

import { IApply } from "./IApply";

export interface IChain<T> extends IApply<T>{
    chain<U>(fn: (T)=> U)
}

chain 메서드는 functor의 map과 다르게 엔도펑터로 구현할 필요가 없다.

import { Identity } from "../classes/Identity";
import { IChain } from "../interface";

console.log(
    Identity.of(1).map(value => `the count is ${value}`).value(),
    Identity.of(1).chain(value => Identity.of(`the count is ${value}`).value())
)

위 코드를 실행하면 같은결과가 나오지만
서로 다르게 사용하는 모습을 볼 수 있다.

엔도펑터인 map은 항상 같은 카테고리에 머무르므로 위와 같이 작성할 수 있지만

chain은 자신이 머무르고 싶은 카테고리를 스스로 정해야 하므로 map과 다르게 작성되는 모습을 볼 수 있다.

IMonad<T> 인터페이스 구현

판타지랜드 규격에서 모나드는 체인과 애플리커티브를 구현한 것이다.

import { IChain } from "./IChain";
import { IApplicative } from "./IApplicative";

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

이제 identity 모나드를 완성시킬 수 있다.

import { IMonad,ISetoid } from "../interface";

export class Identity<T> implements ISetoid<T>,IMonad<T>{
    constructor(private _value:T){}
    //IValuable
    value() {return this._value}
    //ISetoid
    equals<U>(that: U): boolean {
        if(that instanceof Identity)
            return this.value() == that.value()
        return false
    }
    //IFunctor
    map<U>(fn: (x:T) => 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))
    }
    //IApplicative
    static of<T>(value: T): Identity<T> {return new Identity<T>(value)}
    //IChain
    chain<U>(fn: (T) => U):U {return fn(this.value())}
}

완성된 Identity 모나드의 모습이다.

다음코드는 모나드의 왼쪽법칙을 충족하는지 보여주는 코드이다.

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)
)

따라서 Identity 모나드는 오른쪽법칙과 왼쪽법칙을 모두 충족하므로 정상적인 모나드라고 할 수 있다.

0개의 댓글