글의 목적
- 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 |