병렬적 처리
지금까지는 프로미스의 resolve() 함수가 결과를 바로 만들어내도록 해서 콜스택이 비워지면 프로미스들이 즉시 처리되도록 예제가 구성되었었다. 만약 프로미스가 시간이 어느정도 걸리는 작업들이라면 지연평가에서는 어떻게 처리되고 있을까?
const delay1000 = a => new Promise(resolve => {
setTimeout(() => resolve(a), 1000);
})
console.time('');
go([1,2,3,5,5,5,5],
L.map(a => delay1000(a*a)),
L.filter(a => a%2),
reduce(add),
console.log,
a => console.timeEnd(''));
위 코드는 go함수의 첫번째 인자인 배열 내부에 있는 원소 갯수 당 1초씩 걸려서 총 7초가 걸리는 작업이 될 것이다. 이유는 위 코드가 지연평가로 실행되기 때문이다. 강의 처음에 만들었던 L.map과 map, L.filter와 filter 들의 차이는 일반 map filter는 진행방향이 가로로 실행되는 것이고 L 시리즈는 세로로 실행되는 것이다 라고 이해했던것을 떠올리면 되겠다. reduce에서 반복문에 들어갔을때 filter로부터 하나 꺼내오려하면 filter는 map으로부터 받은 이터레이터에서 하나를 꺼내오게 된다. 이때 map으로부터 받는것은 1초가 걸려서 받게되는 작업일 것이다. 이러한 과정을 모든 원소마다 반복해야하니 비효율적으로 처리되는 것이다. 이것을 해결하기 위해서는
const reduce = (f, acc, iter)=> {
if(!iter) {
[acc, ...iter] = [...acc]; //1
iter = acc[Symbol.iterator](); // 2
acc = iter.next().value; // 2
}
for (const a of iter ){
acc = f(acc, a);
}
return acc;
}
이전에 reduce 코드에서 1번방식과 2번방식이 뭐가 다르지..? 했던 것에서 해답이 있었다.. 1번 방식은 전개연산자를 통해서 이터레이터의 값들을 풀어헤친다.
const C = {};
C.reduce = curry((f, acc ,iter) => iter ?
reduce(f, acc , [...iter]) :
reduce(f, [...acc]));
그러므로 위와같이 만들면 전개 연산자를 통해 순회가 일어난 후 reduce 함수가 호출되니 1초씩 걸리는 작업들이 병렬적으로 실행되며 배열안에 몇개의 원소가 있든간에 약 1초가 걸리는 코드가 된다.
하지만 위의 코드는 문제가 하나있다. C.reduce 내부에서 따로 catch를 해주지 않고 있기 때문에 후에 실행될 reduce에서 catch를 해주더라도 별도의 콜스택이라 에러를 핸들링해주지 않고 있다는 에러가 뜨게된다. 그래서 아래와 같은 트릭을 줘야한다.
const catchNoop = arr =>
(arr.forEach(a => a instanceof Promise ? a.catch(noop) : a), arr);
위의 코드가 트릭인 이유는 catch를 적용한 프로미스를 다시 arr에 할당하는게 아니기 때문이다. 그러므로 이후의 콜스택으로 넘어갈 reduce 함수의 인자로는 여전히 catch해주지 않은 프로미스이지만 어쨌든 현재 C.reduce 내에서는 캐치를 해주고는 있기 때문에 에러로그가 발생하지 않게된다.
C.filter C.map
병렬적으로 동작하는 C.map과 C.filter는 간단하다.
C.takeAll = C.take(Infinity);
C.map = curry(pipe(L.map, C.takeAll));
C.filter = curry(pipe(L.filter, C.takeAll));
C.map(a=>delay1000(a*a), [1,2,3,4]).then(log);
C.filter(a => delay1000(a % 2), [1,2,3,4,5]).then(log);
이제 지연평가 함수와 병렬평가 함수들은 다음과 같이 원하는대로 조합이 가능하다.
const delay500 = (a, name) => new Promise(resolve => {
console.log(`${name} : ${a}`);
setTimeout(()=>resolve(a), 500);
});
go([1,2,3,4,5,6],
L.map(a => delay500(a * a ,'map1')),
L.filter(a => delay500(a % 2 ,'filter2')),
L.map(a => delay500(a * a ,'map3')),
C.take(2), //지연평가들을 한번에 병렬적으로 평가
log)
go([1,2,3,4,5,6],
L.map(a => delay500(a * a ,'map1')),
C.filter(a => delay500(a % 2 ,'filter2')), // L.map으로부터 병렬적으로 가져옴
L.map(a => delay500(a * a ,'map3')), // 이후로는 지연적으로 동작.
take(2),
log)
중간에 병렬적으로 실행되는 C 를 넣어서 원하는 지점까지는 병렬적으로 실행되도록 하는것이 가능하다. 두번째 예제 같은 경우에는 C.filter 이후에 L.map부터는 다시 지연적으로 평가되며 실행될 것이다.
추가 - async, await
async와 await은 비동기 상황들을 동기적인 문장으로 다룰 수 있도록 하는 것이다.
async : 이 키워드를 함수 앞에 붙이면 해당 함수는 프로미스를 리턴한다.
async와 await를 활용하면 해당 함수 내에서만 동기적인 실행이 일어나는 것이다. 따라서 해당 함수의 리턴값으로 다른 어딘가에서 동기적으로 실행되기를 바라는 건 잘못된 것이다.
async function f1(){
const a = 10
const b = 14
return a + b;
}
console.log(f1()); // Promise { 24 }
(async() => {
log(await f1()); // 24
})()
어떤 함수가 프로미스를 리턴했다면 결과값 앞에 await를 붙여주면 위의 예제 즉시실행 함수 내에서 바로 24를 출력해주는 것처럼 동기적으로 실행되는 효과를 볼 수 있다.
마무리
이렇게 함수형 프로그래밍 1편 강의를 마쳤다 😀 자바스크립트 거의 입문한 수준에서 처음 들었던 강의인데 너무 어려워서 중도 포기만 두번했다 ㅋㅋㅋㅋ js 만난지 반년째에 다시 수강하니 다행히도 전보단 더 이해가 되는듯하다. 후 힘들었다.. 그동안 정리한걸 한번에 올리니 뭔가 뿌듯하기도 하고 누가 보면 하루만에 이해한 줄 알겠다 ㅋㅋㅋㅋ 완강했다해도 여전히 경이롭다 어떻게 저렇게 코딩하는지.. 🤯🤯 강사님은 어나더레벨이라는 것을 강의 볼때마다 느낀다. 담주부터는 강의 2편을 들어볼까한다. 1편보다 훨씬 어렵다는데 각오 단단히 하고 시작해야겠다.
관련글
https://www.inflearn.com/course/functional-es6 강의를 들으며 정리한 글입니다.
'함수형 프로그래밍' 카테고리의 다른 글
자바의 함수형 프로그래밍 (0) | 2021.09.24 |
---|---|
JS 함수형 프로그래밍 - 비동기, Promise [2] (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 비동기, Promise [1] (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 지연성 (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 이터러블, 이터레이터, 제너레이터 (0) | 2021.08.01 |