[함수형 프로그래밍] - 모나드

2021년 02월 26일, 20:30

모나드가 중요한 이유?

  • 모나드는 사이드 이펙트를 결과 집합과 함께 포함하는 타입
  • 함수형 프로그래밍에서 사이드 이펙트가 생기는 경우를 방지하기 위해 사용
      f:FileName→FILE 인 경우 file이 존재하지 않는다면 에러를 발생시킨다.
    
      Error의 값을 다음 함수로 전달 하는 것은 다음 함수의 인자와 맞지 않기 때문에 순수 함수성이 깨진다고 볼 수 있다.
      (다음 함수는 file이 올거라고 기대하기 때문)
    
      하지만, 모나드를 사용하여 f:FileName→Monad[FILE,Error]와 같이 file과 에러를 같이 포함하는 모나드를 만든다면
      다음 함수는 모나드를 받아 처리하고, 그 함수는 또 모나드를 반환한다면 순수 함수성을 지킬 수 있다.
    
  • 사이드 이펙트가 생긴다면 순수 함수의 특성이 깨지고, 함수의 합성이 어려워짐
  • 함수의 값을 모나드로 받고, 실행 결과도 모나드가 된다면 순수 함수가 되고 함수의 합성을 통해 흐름을 만들 수 있음

Functor

  • 인자를 받아 Functor를 반환
  • map 메소드를 가지고 있음.
    function Maybe(value) {
        this.value = value;
        this.map = function (fn) {
            return this.isNothing ? this : Maybe.of(fn(this.value));
        };
    }
    
    Maybe.of = function (value) {
        return new Maybe(value);
    };
    
    Object.defineProperty(Maybe.prototype, 'isNothing' , {
        get: function(){
          return this.value === null || this.value === undefined;
        }
    });
    
  • Maybe functor 예제코드
    const log = console.log;
    const curry = (f) => (a, ...bs) => (bs.length ? f(a, ...bs) : (...as) => f(a, ...as));
    const curryR = (f) => (a, ...bs) => (bs.length ? f(a, ...bs) : (...as) => f(...as, a));
    
    function reduce(f, iter, acc) {
        if (arguments.length === 2) {
          iter = iter[Symbol.iterator]();
          acc = iter.next().value;
        }
        for (const e of iter) {
          acc = f(acc, e);
        }
        return acc;
    }
    
    const go = (a, ...fs) => reduce((a, f) => f(a), fs, a);
    
    function Maybe(value) {
        this.value = value;
        this.map = function (fn) {
          return this.isNothing ? this : Maybe.of(fn(this.value));
        };
    }
    
    Maybe.of = function (value) {
        return new Maybe(value);
    };
    
    Object.defineProperty(Maybe.prototype, 'isNothing' , {
        get: function(){
          return this.value === null || this.value === undefined;
        }
    });
    
    const map = curry((fn, functor) => functor.map(fn));
    
    const books = [
        { id: "12", name: "the lord of the rings" },
        { id: "34", name: "harry poter" },
    ];
    
    const findBookById = curry((id, books) => {
        log("findBookById");
        return books.find((book) => book.id === id)
    });
    
    const get = curry((key, obj) => {
        log("get");
        return obj[key];
    });
    
    const upperCase = (str) => {
        log("upperCase");
        return [...str].map(e => e.toUpperCase()).join('')
    };
    
    const _val = (functor) => functor.value;
    
    go(
        books, 
        Maybe.of,
        map(findBookById("12")),
        map(get("name")),
        map(upperCase),
        log
    );
    
    // 여기서 문제, 값에 예외가 있는 경우?
    // get, upperCase에 모두 다 예외처리를 할 수 없다.
    
    log("------------------------------");
    
    go(
        books,
        Maybe.of,
        map(findBookById("56")),
        map(get("name")),
        map(upperCase),
        log
    )
    

Monad

  • Functor에서 flatMap이 추가
      function Maybe(value) {
          this.value = value;
          this.map = function (fn) {
            return this.isNothing ? this : Maybe.of(fn(this.value));
          };
          this.toString = function(){
            return this.isNothing ? `Nothing` : `Just(${this.value})`;
          }
          this.flatMap = function(fn){
            return this.isNothing ? this : fn(this.value);
          }
      }
    
      Maybe.of = function (value) {
          return new Maybe(value);
      };
    
      Object.defineProperty(Maybe.prototype, 'isNothing' , {
          get: function(){
            return this.value === null || this.value === undefined;
          }
      });
    
  • flatMap을 통해 중첩된 Functor를 중첩된 구조를 풀어줌
  • Maybe monad 예제코드
  const log = console.log;
  const curry = (f) => (a, ...bs) => (bs.length ? f(a, ...bs) : (...as) => f(a, ...as));
  const curryR = (f) => (a, ...bs) => (bs.length ? f(a, ...bs) : (...as) => f(...as, a));

  function reduce(f, iter, acc) {
      if (arguments.length === 2) {
        iter = iter[Symbol.iterator]();
        acc = iter.next().value;
      }
      for (const e of iter) {
        acc = f(acc, e);
      }
      return acc;
  }

  const go = (a, ...fs) => reduce((a, f) => f(a), fs, a);

  const pipe = (...fs) => (a) => reduce((a, f) => f(a), fs, a);

  const sum = pipe((a) => a+1, (a) => a+2);

  const books = [
      { id: "12", name: "the lord of the rings", author: "J.R.R. Tolkien" },
      { id: "34", name: "harry poter", author: "J.K. Rowling" },
  ];

  function Maybe(value) {
      this.value = value;
      this.map = function (fn) {
        return this.isNothing ? this : Maybe.of(fn(this.value));
      };
      this.toString = function(){
        return this.isNothing ? `Nothing` : `Just(${this.value})`;
      }
      this.flatMap = function(fn){
        return this.isNothing ? this : fn(this.value);
      }
  }

  Maybe.of = function (value) {
      return new Maybe(value);
  };

  Object.defineProperty(Maybe.prototype, 'isNothing' , {
      get: function(){
        return this.value === null || this.value === undefined;
      }
  });

  const map = curry((fn, functor) => functor.map(fn));

  const flatMap = curry((fn, monad) => monad.flatMap(fn));

  const findBookById = curry((id, books) => {
      return books.find((book) => book.id === id)
  });


  // f:filename -> Monad[file,Error]
  const getBookById = curry((id, books) => 
      pipe(
        findBookById(id),
        Maybe.of,
      )(books));

  const validateBookAuthor = (book) => book.author.indexOf('Rowling') > -1 ? Maybe.of(book) : Maybe.of(null);

  const logProp = curry((key, obj) => {
      log(obj[key])
      return obj;
  });

  // 함수가 functor를 인자로 받고 functor를 반환해버리면 functor의 value값이 functor가 된다.

  go(
      books,
      getBookById("34"), // -> Maybe
      map(validateBookAuthor), // -> Maybe(Maybe)
      map(map(logProp("name"))),
  )

  // 해결책 functor.value로 풀어주면 된다.

  // go(
  //   books,
  //   getBookById("34"),
  //   map(validateBookAuthor),
  //   functor => functor.value,
  //   map(logProp("name"))
  // )

  // 해결책2 flatMap

  go(
      books,
      getBookById("34"), // functor
      flatMap(validateBookAuthor), // functor
      flatMap(logProp("name")),
      log
  )

Monad의 종류

  • Optional/Maybe
  • Either
  • Future

참고