본문 바로가기

데일리

[23.10.25] Promise 함수 합성(Monad, Kleisli Composition)

글의 목적

  • myfx라는 node에서 내가 사용하기 위해서 정리한 함수를 더 잘 이해하고 함수합성에서 어떤 목적을 가진 합성인지를 정리하려고 함.
  • 그때 필요한 용어인 Monad와 Kleisli Composition에 대해서 정리하려고 함.

본론

1. Monad

내가 정리하는 Monad는 완벽한 정의가 아니며 완벽한 Monad를 이해하고 싶다면 책을 보는것을 추천한다.

이 글에서는 합성과 관련된 Monad의 의의에 집중하려고 한다.

  • Monad는 일종의 컨테이너. 즉, 박스다.
  • Monad라고 하는 것은 박스가 가진 메서드를 활용해서 함수 합성을 진행하는 것
  • 그리고 박스(Monad)는 여러 연산에 필요한 재료를 담고 있다. 
  • 예를 들어 보자, 함수 add1과 mul이 있다
const add1 = a => a + 1;
const mul = a => a * a;
  • 이 두 함수를 함수를 합성한다고 하면 이렇게 합성할 수 있겠다.
const log = console.log;
const add1 = a => a + 1;
const mul = a => a * a;

log(mul(add1(1))); // 4
  • 그런데 만약 이 함수에 값이 안들어가면 어떻게 될까?
log(mul(add1())); // ???
  • 정답은 NaN이 찍히게 된다.
  • 이 말은 즉, 외부 세상에 영향을 주고 싶지 않은 값도 영향을 주게 된다는 뜻이다.
  • 그럼 함수 합성을 값이 항상 안전할 때에만 해야한다는 말인데 이는 매우 비효율적이고 에너지가 많이 소모되는 일이된다.
  • 그럼 어떻게 하면 값이 있을지 없을지 모르는 상황에서 안전하게 함수 합성을 할 수 있을까?
  • 그 때 필요한 것이 바로 Monad이다.

1.1 함수 함성에서의 Array Monad

  • Array Monad의 핵심 목표는 값이 있는지 없는지 모르는 상황에서의 안전하게 함수를 합성하는것이다.
const log = console.log;
const add1 = a => a + 1;
const mul = a => a * a;

log(mul(add1(1))); // 4
log(mul(add1())); // NaN
  • Array에서의 Manad는 무엇일까?
[] // Monad이다.
  • 허탈하겠지만 저것이 모나드고 Array에서는 함수 합성을 map으로 한다. 
const log = console.log;
const add1 = a => a + 1;
const mul = a => a * a;

[1].map(add1).map(mul).forEach(r => log(r)) // 4
  • 함수합성이 잘 되었고 그럼 이번에 값을 안줘보겠다.
const log = console.log;
const add1 = a => a + 1;
const mul = a => a * a;

[1].map(add1).map(mul).forEach(r => log(r)) // 4
[].map(add1).map(mul).forEach(r => log(r)) //
  • 조금 황당하겠지만 연산에 필요한 값이 없다면 아무일도 일어나지 않는다.
  • 즉, 외부세상에 전혀 영향을 주지 못하는것이고 이는 보다 더 안전하게 합성이 되었다는 의미이다.

1.2 함수 함성에서의 Promise Monad

1.2.1 Callback방식과 다른 Promise 의 핵심

  • 이 글을 정리하기 전에 Promise에 대해서 정리를 먼저 했어야 했나? 라는 고민을 잠깐 했다.
  • 왜냐면 Promise의 핵심을 정리하지 않고 이 글을 보면 Promise가 가진 진정한 이유와 목적이 잘 안느껴지기 때문이다.
  • 그렇지만 그럼 글을 가벼운 마음을 적을수가 없기 때문에 그냥 여기서 간단하게 Promise의 핵심만 정리하려고 한다.
  • Promise란 무엇인가?
  • 다들 콜백지옥을 대신하기 위해서 만들어진 좀더 간편한 비동기 처리 장치라고 생각할 것이다.
  • 하지만 틀렸다. Promise의 진정한 의미는 바로 비동기상황을 일급값으로 처리한다는 의미이다.
  • 예를 들어 보겠다. 다시한번 기억해라 Promise의 핵심은 값으로서 비동기상황을 제어할 수 있다는 의미이다.
// callback 방식
const add1delay100 = (a, cb) => {
  setTimeout(() => cb(a + 1), 100);
};
add1delay100(5, (res) => {
  add1delay100(res, (res) => {
    add1delay100(res, (res) => {
      add1delay100(res, (res) => {
        add1delay100(res, (res) => {
          add1delay100(res, (res) => {
            add1delay100(res, (res) => {
              log(res); // 12
            });
          });
        });
      });
    });
  });
});

// Promise 방식
const add2delay100 = (a) => {
  return new Promise((resolve) => setTimeout(() => resolve(a + 2), 100));
};
add2delay100(5)
  .then(add2delay100)
  .then(add2delay100)
  .then(add2delay100)
  .then(add2delay100)
  .then(add2delay100)
  .then(add2delay100)
  .then(log); // 19
  • 위에 예시 코드를 보고 느끼는것이 "와 Promise방식이 훨씬 간단하다" 가 아니고 "와 Promise 방식은 Promise를 return해서 값으로서 비동기상황을 제어하는구나" 가 맞는것이다. 

그럼 다시 본론으로 넘어가서 함수 합성에서의 Promise Monad를 살펴보겠다.

  • Promise에서의 함수 합성도 Array에서의 합성과 매우 유사하다.
  • Array에서는 map으로 안전하게 합성을 했다면, Promise then으로 안전하게 합성을 한다.
  • 하지만 여기서 말하는 안전이 조금 다르다. (Monad의 목적이 다르다)
// 둘이 매우 유사한것을 확인할 수 있다.
// Array Monad
[1].map(add1).map(mul).forEach(r => log(r)) // 4
[].map(add1).map(mul).forEach(r => log(r)) //

// Promise Monad
new Promise((resolve) => setTimeout(() => resolve(1), 100))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
new Promise((resolve) => setTimeout(() => resolve(), 100))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // NaN
  • 글을 처음부터 잘 읽어내려온 독자라면 위에 결과에 대해서 ???라는 생각이 들것이다.
  • 이유는 바로 NaN이 나왔기 때문.
  • 그러나 위에서 안전과 Monad의 목적이 다르다고 한 말을 기억한다면 이 결과가 맞는 결과이다.
  • Promise에서의 함수합성과 Monad는 값이 있는지 없는지가 아니다 delay되는 즉, 비동기에 시간이 있든 없든이다. 
  • Promise의 then은 매우 안전하게 합성을 해주고 있다.
  • 왜냐면 몇초가 delay되던 안전하게 비동기상황을 제어하고 함수합성 결과를 내기 때문이다.
// Promise Monad
new Promise((resolve) => setTimeout(() => resolve(1)))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
new Promise((resolve) => setTimeout(() => resolve(1), 10))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
new Promise((resolve) => setTimeout(() => resolve(1), 100))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
new Promise((resolve) => setTimeout(() => resolve(), 1000))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
new Promise((resolve) => setTimeout(() => resolve(), 10000))
  .then(add1)
  .then(mul)
  .then((r) => log(r)); // 4
  • 매우 안전하다.

1.3 함수 합성에서의 Promise Kleisli Composition

  • 조금 생소한 용어인 Kleisli Composition을 소개하자면 에러상황에서 안전하게 함수합성을 하는것 이라고 매우 간단하게 표현하고 싶다.
  • 어려운 개념은 아니지만 이 부분은 Promise에서도 reject와 catch로서 가능하기 때문에 소개를 하려고 한다.
  • 예를 들어 같은 함수합성이라도 데이터의 따라서 같은 결과가 나오지 않을수도 있다.
  • 그럴때 Kleisli Composition은 같은 결과로 나올 수 있도록 하는것이 바로 Kleisli Composition의 함수 합성 관점이다.
  • 그럼 예를 들어보도록 하겠다.
// posts 데이터
const posts = [
  { id: 1, title: "test1" },
  { id: 2, title: "test2" },
  { id: 3, title: "test3" },
];

// id값으로 post를 불러오는 함수
const getPostById = (id) =>
  find((p) => p.id == id, posts)
  
// 위에 함수가 있을때 title만 가져오기 위해서 함수 a와 b를 만들어서 합성하여 사용하려고 한다.
const a = ({ title }) => title;
const b = getPostById;

const ab = (id) =>
  Promise.resolve(id)
    .then(b)
    .then(a)

// 함수를 합성하였고 원하는 title이 출력된다.
ab(2).then(log) // test2
  • 그런데 이 상황에서 사용자가 글을 지운다고 한다면 똑같은 함수 합성이더라도 동일한 결과를 보장하지 않는다.
// posts 데이터
const posts = [
  { id: 1, title: "test1" },
  { id: 2, title: "test2" },
  { id: 3, title: "test3" },
];

// id값으로 post를 불러오는 함수
const getPostById = (id) =>
  find((p) => p.id == id, posts)
  
// 위에 함수가 있을때 title만 가져오기 위해서 함수 a와 b를 만들어서 합성하여 사용하려고 한다.
const a = ({ title }) => title;
const b = getPostById;

const ab = (id) =>
  Promise.resolve(id)
    .then(b)
    .then(a)
    
posts.pop(); // test1 제거
posts.pop(); // test2 제거

// 함수를 합성하였고 원하는 title이 출력된다.
ab(2).then(log) // Error
  • 이러한 상황에서 reject로 동일한 Promise{<rejected>}라는 값을 받을수 있고 catch로 사용자에게 동일한 처리를 할 수 있게 되는것. 그것이 Kleisli Composition의 관점에서의 함수 합성이라고 말하고 싶다.
// posts 데이터
const posts = [
  { id: 1, title: "test1" },
  { id: 2, title: "test2" },
  { id: 3, title: "test3" },
];

// id값으로 post를 불러오는 함수
// Promise.reject로 동일한 Promise값 생성
const getPostById = (id) =>
  find((p) => p.id == id, posts) || Promise.reject("delete됨");
  
// 위에 함수가 있을때 title만 가져오기 위해서 함수 a와 b를 만들어서 합성하여 사용하려고 한다.
const a = ({ title }) => title;
const b = getPostById;

// catch로 에러 처리
const ab = (id) =>
  Promise.resolve(id)
    .then(b)
    .then(a)
    .catch((e) => e);
    
posts.pop(); // test1 제거
posts.pop(); // test2 제거

// 함수를 합성하였고 동일한 결과가 출력된다.
ab(2).then(log) // delete됨
  • 혹시 위에 사용된 find 함수가 궁금하시면 제 github에서 myfx를 clone하십시오!

결론

  • Promise에 대한 이해와 함수형 프로그래밍의 안정성, 확장성을 이해할 수 있었음.
  • 혹시 monad와 Kleisli Composition에 대한 나의 설명이 부족하게 느껴지실수도 있는데 그렇게 느껴지는것이 아니라 정말 부족한것임.
  • 이 글은 monad  Kleisli Composition에 대해서 다루는것이 아닌 그러한 관점에서의 함수 합성을 다루었기 때문!  

'데일리' 카테고리의 다른 글

[23.10.31] js(자바스크립트) 스코프  (0) 2023.10.31
[23.10.29] nodejs, DI, Ioc, nestjs  (67) 2023.10.29
[23.10.21~22] nestjs decorator  (54) 2023.10.22
[23.10.20] 프로토타입  (27) 2023.10.19
[23.10.19] 생성자 함수에 의한 객체 생성  (29) 2023.10.19