비동기를 값으로 만드는 Promise
- promise 클래스를 통해 만들어진 인스턴스를 반환함.
- promise 값은 대기, 성공, 실패를 다루는 일급 값으로 이루어짐.
- 일급이란 어떤 함수, 변수에 전달될 수 있고, 그 값으로 다른 일을 만들어 나갈 수 있음.
- 코드나 콘텍스를 다룰 뿐 아니라 대기되는 값을 만든다는 점에서 callback과 차이가 있음.
- 특히 then은 어떠한 값도 return을 하지 않는 반면 promise는 promise를 반환하여 원하는 일을 다룰 수 있음.
값으로서의 Promise 활용
- promise가 비동기 상황을 값으로 다루는 일급의 성질을 갖고 있다는 걸 활용해보자.
- 값은 함수에게 전달할 수 있고, 해당 하는 값이 어떤 건지, promise인지 아닌지 확인 가능.
const go1 = (a ,f) => f(a); const add5 = a => a + 5; console.log(10, add5)
- a와 f는 동기적으로 동작해야 함.
- 비동기 상황이 아닌 일반 값(promise가 아닌 값)이 들어와야 함수에 값이 잘 적용되어 동작함.
- a 대신
Promise.resolve(10)
를 넣으면 계산을 하지 않음.
다형성을 지원하는 go1 함수 표현
const delay100 = a=> new Promise(resolve => setTimeout(() => resolve(a),100)); const go1 = (a ,f) => a instanceof Promise ? a.then(f) : f(a); const add5 = a => a + 5; var r = go1(delay100(10), add5); r.then(console.log); //15 const n = delay100(10); go1(go1(n, add5), console.log); //15, 헤딩 라인 전체를 log로 찍으면 promise를 반환함.
- instanceof : 생성자의 prototype 속성이 객체의 프로토타입 체인 어딘가 존재하는지 판별함.
- promise가 반환된다는 것은 지속적인 연결이 가능하다는 것을 의미함.
합성 관점에서의 Promise와 모나드
안전한 합성을 위한 모나드와 구현체 중 비동기 상황을 안전하게 실행하기 위한 promise.
어떤게 안전하지 않은 상황일까?
const g = a => a + 1; const f = a => a * a; console.log(f(g(1))); //4 console.log(f(g())); //NaN
- 값의 전달과 함수 실행이 연속적으로 이루어짐.
- 데이터를 받아 출력하는 상황(g에 어떤 값이 올지 모르는 상황)에서 데이터가 들어오지 않았음에도 문제를 일으킴(NaN).
- 또한 숫자와 같이 안전한 인자(함수가 정상 동작할 수 있는 인자)만 들어왔을 때 합성이 가능함.
어떤 값이 올지 모르는 상황에서 안전하게 함수 합성하는 방법 ⇒ 모나드
const g = a => a + 1; const f = a => a * a; Array.of(1).map(g).map(f).forEach(r => console.log(r)); //4 [1].map(g).map(f).forEach(r => console.log(r)); //4
- 배열안에 값을 넣지 않으면 실행되지 않음.
- forEach 속 함수 자체가 실행되지 않음.
- 이렇게 모나드 방식으로 map을 통해 함수를 합성한다면 안전하게 발생.
Promise는 어떠한 함수 관점에서 합성할까?
Promise.resolve(1).then(g).then(f).then(r => console.log(r)); //4 Promise.resolve().then(g).then(f).then(r => console.log(r)); //NaN
- array를 활용한 방식과 동일하게 동작함.
- Promise는 비동기적인 상황에서 안전하게 합성하기 위한 도구임.
- 단 여기서 값 입력에 대한 안전한 합성이 아닌, 비동기 상황(대기가 일어나는 상황)에서 안전하게 합성하려는 성질을 말하는 것임.
new Promise(resolve => setTimeout(() => resolve(2),100)) .then(g).then(f).then(r => console.log(r))
모나드라는 개념에 집중할 필요 없음.
1) 불안전한 상황에서 연속적인 함수를 안전하게 실행하고,
2) 그렇게 만들어진 값으로 어떤 효과를 만들기 위해 그 전까지 안전하게 합성하기 위한 도구임.
이때 합성 관점에서 promise는 비동기적인 상황에서 쓰이는 모나드임.
Kleisli Composition 관점에서의 Promise
- promise는 kleisli composition을 지원하는 도구임.
- 함수 합성 방법인 kleisli composition은 오류가 있을 수 있는 상황에서 안전하게 함수를 합성하기 위한 규칙.
- 중간에 reject, error가 났을 때 대기중인 함수를 실행하지 않고 맨 뒤로 보냄.
var users = [ {id : 1, name : 'aa'}, {id : 2, name : 'bb'}, {id : 3, name : 'cc'} ]; const getUserById = id => find(u => u.id == id)(users) || Promise.reject('nothing'); const f = ({name}) => name; const g = getUserById; const fg = id => Promise.resolve(id).then(g).then(f).catch(a=>a); //iid. g 등의 데이터가 문제가 있으면 함수를 실행하지 않고 catch 에러를 반환함. 그리고 다음을 실행함. users.pop(); users.pop(); //데이터의 변화를 가정함. fg(2).then(console.log); //nothing
go, pipe, reduce에서 비동기 제어
- 중간에 promise를 만났을 때 다형적 대응과 안전한 합성을 해줌.
- 콜백지옥을 해결하는 용도 뿐만 아니라 promise의 값을 가지고 원하는 로직, 시점을 컨트롤할 수 있음.
//reduce 수정 const go1 = (a,f) => a instanceof Promise ? a.then(f) : f(a); //go 첫번째 인자로 promise가 올 경우를 대비 function reduce(f) { return function (acc, iter) { if (!iter) acc = (iter = acc[Symbol.iterator]()).next().value; else { iter = iter[Symbol.iterator](); } return go1(acc, function recur(acc) { let cur; while(!(cur = iter.next()).done) { const a = cur.value; acc = f(acc, a); if(acc instanceof Promise) return acc.then(recur); } return acc; }); } } //실행 go( Promise.resolve(1), a => a + 10, a => Promise.resolve( a + 100 ), a => a + 1000, console.log ); //1111
promise.then의 중요한 규칙
promise의 중첩 길이가 어떻든 then을 실행하면 promise 안쪽 값을 한번에 꺼냄.
Promise.resolve(Promise.resolve(1)) .then(function(a) {console.log(a)}); //1 new Promise(resolve => resolve( new Promise(resolve => resolve(1)) )).then(console.log); //1
지연 평가 + Promise - L.map, map, take
다음과 같이 go의 첫 번째 인자로 Promise가 올 경우 정상 동작하게 만들어보자.
go([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)], L.map(a => a + 10), take(2), console.log )
- map과 take 메서드에
if(a instanceof Promise)
조건을 넣어 Promise라면 then을 사용함.
Kleisli Composition - L.filter, filter, nop, take
다음과 같이 Promise일 때, 지연 평가도 되며 비동기, 동시성을 모두 지원하는 map, filter, reduce 함수를 만들어보자.
go([1,2,3,4,5,6], L.map(a => Promise.resolve(a*a)), L.filter(a=> a%2), take(2), console.log )
값이 promise로 전달되기 때문에 promise인지 판별하여 값을 풀어주는 과정이 필요.
L.filter = curry(function* (f, iter) { for (const a of iter) { const b = go1(a, f); if (b instanceof Promise) yield b.then(b => b ? a : Promise.reject(nop)); else if (b) yield a; } });
- nop이라는 구분자를 이용하여
Promise.reject(nop)
으로 에러를 전달함 →.catch(e => e == nop ? recur() : Promise.reject(e))
로 에러를 구분함.
reduce에서 nop 지원
지연성과 promise를 모두 지원하는 reduce를 만들어보자.
go([1,2,3,4], L.map(a=> Promise.resolve(a*a)), L.filter(a=> Promise.resolve(a%2)), reduce(add), console.log )
const reduceF = (acc, a, f) => a instanceof Promise ? a.then(a => f(acc, a), e => e == nop ? acc : Promise.reject(e)) : f(acc, a);
지연된 함수열을 병렬적으로 평가하기
- c는 concurrency를 의미함.
- (병렬평가 말해줄때 다시 정리)
C.reduce, C.take 1
const delayRandom = a=> new Promise(resolve => { setTimeout(() => { console.log('hi', a) resolve(a) }, Math.random()*5000) }); go([1,2,3,4,5], L.map(a=> delayRandom(a*a)), L.filter(a=>a%2), C.reduce(add), console.log );

- 병렬 평가 되기 때문에 우선 실행되는 함수에 한해서 catch error를 사용해 나중에 평가됨을 말해줘야 함. 임시적으로 앞에서 catch를 해두는 것임.