Monad 간단 소개

김장훈·2023년 1월 7일
0

개요

들어가기에 앞서

  • 함수형 프로그래밍 특징 혹은 지향점을 이야기 하자면 순수 함수참조 투명성 준수라고 할 수 있다.
    • 예) 5 = add(2,3) -> add(2,3) + 5 처럼 사용할 수 있는가?
  • error 가 throw 되는 것은 이를 위반하는 것이다.
  • 그렇기에 순수함수를 준수하기 위해서라도 error 가 handling 되어야 하며 이를 monad 를 통해 처리한다고 할 수 있다.

함수자 (functor)

정의(개념)

  • 기본적인 객체로서 객체 내의 각 값을 실행할때 같은 객체를 return하는 map interface 를 구현 한 것
  • 불안전한 값을 감싸는 container 의 개념
    // Array 도 일종의 functor 의 개념
    const first = [1,2,3].map((x:number)=>x) // array
    const second = first.map((x:number)=>x) // array
  • 컨테이너를 열고 보관된 값에 주어진 함수를 적용한 다음 그 결과를 동일한 형식의 새 컨테이너에 넣고 닫는다.
    • 같은 객체(=형식, 타입)가 반환 되므로 chainning 이 가능하다.
      obj.map().map().map()

사용하는 이유

  • 값에 대한 직접적인 접근을 차단한다.
    • map 을 통해서만 값에 대한 접근이 가능함
  • 에러가 날지도 모를 값을 wrapper 객체로 감싸는 것
  • 구현 예시
    class Wrapper{
    	constructor(value){
    	  this._value = value
    	}
    
    	map(f){
    	  return f(this._value)
     }
    }
    
    const wrappedValue = new Wrapper('Wrapper')
    wrappedValue.map(console.log) // "Wrapper"
    wrappedValue.map((x)=>(x.toUpperCase()) // "WRAPPER"

모나드(monad)

개념

  • 함수자로서, 컨테이너 안으로 값을 승급하고 어떤 규칙을 정해 통제한다는 생각으로 자료형을 생성하는것
  • 모나드 마다 각각의 개성, 특징, 목적이 별개로 존재
    • Maybe(또는 Option), Either 등
  • 모든 모나드는 아래의 interface 를 구현해야함
    • 형식 생성자: 모나드 형을 생성한다, 생성자
    • 단위 함수: 어떤 형식의 값을 모나드에 삽입, of
    • 바인드 함수: 연산을 서로 체이닝하는 것, map
    • 조인 연산: 모나드 내의 계층 구조를 단순화 join or flatMap

      nested 자료구조를 단순하게 만드는 것

구현

class ExampleMonad {
	constructor(value){
	  this._value = value
	}
	
	of(value){ // 단위 함수
	  return new ExampleMonad(value)
	}
	
	map(f){ // 바인드 함수
	  return ExampleMonad.of(f(this._value))
	}
	join(){ // 조인 연산
	  return this._value
	}
}

const example = ExampleMonad.of('value')
const example2 = example.map((val)=> val.toUpperCase())
// ExampleMonad 객체가 return

MayBe(Option) , Either(Try)

  • 공통점
    • 둘다 예외처리를 위한 방법
    • 함수자
  • 차이점
    • Maybe(Option)
      • value 의 존재 여부를 보장함
      • one or none
      • 왜 존재하지 않는지에 대한 이유는 모름
    • Either
      • value 또는 error 를 포함하는 자료구조
      • one or the other, but not both

MayBe(Option 으로도 많이 쓰임)

개념

  • None or Undefined 등 값이 없는 경우를 대비하기 위함

구현

export class MayBe {
  _value: any;
  constructor(value: any) {
    this._value = value;
  }

  static of(value: any) {
    return new MayBe(value);
  }

  isNothing() {
    return this._value !== null;
  }

  map(f: CallableFunction) {
    return this.isNothing() ? MayBe.of(f(this._value)) : MayBe.of(null);
  }

  join() {
    return this.isNothing() ? MayBe.of(null) : this._value;
  }

  chain(f: CallableFunction) {
    return this.map(f).join();
  }
}

사용 예시

let res = Maybe.of('George')
      .map((x: string) => x.toUpperCase())
      .map((x: string) => 'Mr. ' + x);
// MayBe { value: 'Mr. GEORGE' }

res = Maybe.of('George')
      .map(() => undefined)
      .map((x: string) => 'Mr. ' + x);
// MayBe { value : null }

res = Maybe.of('George')
      .map(() => undefined) // A funtion
      .map((x: string) => x)
      .map(() => undefined) // B function
      .map((x: string) => 'Mr. ' + x);
// MayBe { value : null }
  • MayBe 내의 값을 변화 시키기 위해선 map 을 호출 해야하며 map 은 항상 value 에 대한 nullable 여부를 확인한다.
  • 따라서 channing 되는 중에 null 이 나온다고 하더라도 다른 logic 들의 영향을 주지 않는다.
  • 다만 값에 대한 보장을 해줄 뿐, 이 값이 왜 없는지 등에 대해선 알 수 없다.

Either(Try 로도 쓰여짐)

개념

  • 양립 불가능한 두개의 값을 논리적으로 구분한 자료 구조
    • Left → 실패, 에러 등 예외 객체를 담는다.
    • Right → 성공한 값을 담는다.
  • 총 3가지의 자료구조로 구현됨
    - Either, Left, Right

구현

export class Left extends Either{
  get value(){
    throw new Error('no value')
  }
  getOrElse(other:any){
    return other
  }
  getOrElseThrow(other:any){
    throw new Error(other)
  }
  map(_f:CallableFunction){
    return this
  }
}

export class Right extends Either{
  get value(){
    return this._value
  }
  
  getOrElse(_other:any){
    return this._value
  }
  getOrElseThrow(other){
    return this._value
  }
  
  map(f:CallableFunction){
    return Either.of(f(this._value))
  }
}

export class Either {
 _value:any
  constructor(value:any){
    this._value = value
  }
  static of(value:any){
    return Either.right(value)
  }
  static left(value:any){
    return new Left(value)
  }
  static right(value:any){
    return new Right(value)
  }
  static fromNullable(value:any){
    return value !== null && value !== undefined ? Either.right(value) : Either.left(value)
  }
}

사용예시

const identity = (value: any) => Either.fromNullable(value).map((x: any) => x);

const res = identity('init-value').getOrElseThrow('error is occur') // init-value

const error = identity(null).getOrElseThrow('null is given') // new Error('null is given')

개념적인 부분은 위와 같다. 하지만 개념적인 부분과 실제사용은 확실히 다른법,
실제사용부분에 대해서 다음 글에서 다루도록 하겠다.

사용예시2

  • 정상 data(right) / 에러 data(left) 를 준비
  • pipe 사용시 각 단계(map) 이 어떻게 반응하는지 확인해보자
    • ps) pipe 는 함수 합성을 사람이 읽기 쉽게 순서대로 바꾼것, pipe(data, add, divide) = data 를 add 하고 divide 한다
  const validData = E.right({ msg: 'valid data' });
  const wrongData = E.left({
    msg: 'error occur',
    error: new Error('some error'),
  });

  pipe(
    validData, // wrongData
    E.map((data: any) => {
      data['some'] = 'real';
      console.log('first process', data);
      return data;
    }),
    E.map((data) => {
      console.log('middle process', data);
      return data;
    }),
    E.match(
      (e) => {
        console.log(`warning, error occur!`);
        return e;
      },
      (data) => data
    ),
    (data) => console.log('result is', data)
  );
  • 정상 data 가 들어왔을 경우
  console.log
    first process { msg: 'valid data', some: 'real' }

  console.log
    middle process { msg: 'valid data', some: 'real' }

  console.log
    result is { msg: 'valid data', some: 'real' }
  • 정상적으로 모든 process 를 거친다.
  • error data가 들어왔을 경우
  console.log // error 가 발생했을시 notification 알람 등을 보내는 형태로도 발전 가능
    warning, error occur!

  console.log
    result is {
      msg: 'error occur',
      error: Error: some error
          ...some error trace
    }
  • 중간에 포함된 process 는 작동 되지 않으며(무시) 결과물로만 표현 된다.
  • error 정보가 포함되므로 timestamp 등의 자세한 정보를 더 추가하면 사용처에서는 추가 작업 없이 error 를 쉽게 추적가능하다.
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

0개의 댓글