본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.
go([1, 2, 3, 4, 5, 6], L.map(a => Promise.resolve(a * a)), L.filter(a => a % 2), L.map(a => a * a), take(2), log); // Promise reject에 같이 넣어보낼 symbol const nop = Symbol('nop'); // L.filter의 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; } }) // take의 Promise 대응 const take = curry((l, iter) => { let res = []; iter = iter[Symbol.iterator](); return function recur() { let cur; while(!(cur = iter.next()).done) { const a = cur.value; if (a instanceof Promise) { return a .then(a => (res.push(a), res).length == l ? res : recur()) .catch(e => e == nop ? recur() : Promise.reject(e)); } res.push(a); if (res.length == l) return res; } return res; } (); })
- 재귀적 유명함수를 return에 사용해 길이가 l이 될 때까지 res 배열에 a를 푸시
- 그런데 만약 Promise의 인스턴스여서 return된 a가 reject라면 내용을 확인
- 만약 nop이면 위의 filter에서 이후를 더 진행해야 한다는 의미의 reject이므로 recur()을 재호출
- nop이 아니라면 그 외의 상황으로 오류가 난 것이므로 Promise.reject(e)를 그대로 반환하여 이후 함수 진행 중단
- 함수열 중간에 Promise 객체가 reject가 반환되면 이후의 .then 함수열은 실행하지 않고 바로 .catch문으로 진입해 코드 실행
// 기존 코드 const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.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; }); });
// Promise 대응 reduce const reduceF = (acc, a, f) => { a instanceof Promise ? // a가 Promise instance인 경우 then으로 풀어 값으로 평가한 뒤 f(acc, a) a.then(a => f(acc, a), e => e == nop ? acc : Promise.reject(e)) : // a가 Promise instance가 아닌 경우 값이므로 바로 f(acc, a) f(acc, a) } const head = iter => go1(take(1, iter), ([h]) => h); const reduce = curry((f, acc, iter) => { if (!iter) return reduce(f, head(iter = acc[Symbol.iterator]()), iter); iter = iter[Symbol.iterator](); return go1(acc, function recur(acc) { let cur; while (!(cur = iter.next()).done) { acc = reduceF(acc, cur.value, f); if (acc instanceof Promise) return acc.then(recur); } return acc; }); });
-
recur 함수 내부에서 a와 acc를 처리할 때, 비동기 처리가 필요
-
해당 부분의 함수가 길어지므로 별도의 함수(reduceF)로 작성한 뒤 호출
-
a가 Promise의 instance인지 판단하여 지연성을 두고 다르게 처리하는 것은 이전의 refactoring과 동일
-
then(a => f1(a), e => f2(e))
- f1: 이전의 로직의 반환값을 인자로 다음 로직 진행
- f2: 이전이 만약 reject를 반환했다면, 해당 reject에 대한 로직 진행
- catch 문을 then 문에 합성하는 개념
-
첫 값이 없는 경우 꺼내는 함수(head)를 작성해 로직 간소화 및 비동기 처리
-
첫 값을 뽑아 다시 reduce에 인자로 전달하며 진행
- 자바스크립트는 싱글스레드이기 때문에 동시에 여러 작업은 불가능하고 비동기 IO로 처리
- 하나의 스레드에서도 CPU를 점유하는 작업을 보다 효율적으로 처리
- 한 node 환경이 아니라, 네트워크나 브라우저, 기타 IO 등 외부 환경으로 작업을 보내 한 번에 처리한 뒤, 이 결과를 받음
C : Concurrency 병행성
const C = {}; C.reduce = curry((f, acc, iter) => iter ? reduce(f, acc, [...iter]) : reduce(f, [...acc]));
- iterator가 있는 경우 [...iter]
를, 없는 경우 [...acc](이것이 곧 iter)를 reduce에 인자로 전달 - 비동기성을 고려하지 않고, 모든 배열을 평가해 reduce로 넘긴다는 것
console.time(''); go([1, 2, 3, 4, 5], L.map(a => delay1000(a * a)), L.filter(a => a % 2), C.reduce(add), log, _ => console.timeEnd('')); // 1005.9492...ms
- L.map과 L.filter를 사용하므로 go함수 내부에서 세로(함수별로 하나씩) 방향으로 작업을 처리
- 때문에 기존 reduce를 사용하면 매번 delay1000 함수 실행
- C.reduce를 활용하면 배열 내 요소 각각마다 delay1000 함수를 실행하는 것이 아니라, 배열을 펼쳐 한 번에 평가 후 다음 함수로 전달
- 함수를 진행하다보면 이후에 비동기적으로 Symbol(nop)을 인식해 이후 작업을 처리해준다.
- 하지만 이후에 이를 처리하더라도 일단 reject가 넘어갔으니 console에 error가 찍히게 된다.
- 이를 방지하기 위해 "뒤에서 비동기적으로 처리할거야"라는 메시지를 call stack에 전달하는 방법을 알아보자.
C.reduce = curry((f, acc, iter) => iter ? reduce(f, acc, catchNoop([...iter])) : reduce(f, catchNoop([...acc])));
- 임시로 catch를 해두는 방식으로 해결
주의할 것 : catch된 것을 보내면 이후에 catch 불가능
C.reduce = curry((f, acc, iter) => { let iter2 = iter ? [...iter] : [...acc]; iter2 = iter2.map(a => a.catch(function () {})); 🔆 ... });
- catch가 이미 된 iterator는 이후에 또 다시 catch 불가능
- **오류 기록을 위한 명색뿐인 catch를 하는 것일 뿐, catch 이전의 Promise를 그대로 전달하는 것
정리
const C = {}; function noop() {} const catchNoop = arr => (arr.forEach(a => a instanceof Promise ? a.catch(noop) : a), arr); C.reduce = curry((f, acc, iter) => iter ? reduce(f, acc, catchNoop([...iter])) : reduce(f, catchNoop([...acc])));
- noop : 아무 일도 하지 않는 함수
- catchNoop : 배열을 받아 배열 요소 각각 확인하여 Promise instance이면 a.catch(noop)한 것들의 배열을 받는 함수
- 이를 iter 유무에 따라 즉시 평가한 배열들을 인자로 넣어 반환
C.take = curry((l, iter) => take(l, catchNoop([...iter])));
- 배열의 최대 길이와 배열을 인자로 전달받는다.
- 이를 take 함수 취한 것을 반환
- l은 그 자체로 최대 길이, 배열은 병렬 처리를 위해 모두 spread하여 즉시 평가 후 catchNoop에 전달
- 이 반환값을 다시 반환
즉시 병렬적으로 평가
특정 함수 라인에서만 병렬적으로 실시, 나머지는 동기적으로 할 때
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) // 1초 뒤에 [1, 4, 9, 16] C.filter(a => delay1000(a % 2), [1, 2, 3, 4]).then(log) // 1초 뒤에 [1, 4, 9, 16]
