자바스크립트는 비동기 IO 동작을 합니다. 이는 하나의 쓰레드에서 IO작업을 효율적으로 처리할 수 있도록 하기 위함입니다. 하지만 데이터베이스 같은 외부에 IO 작업을 요청하는 경우에는 그저 명령을 전달 후 완료 시점을 대기하는 상황이기 때문에 자바스크립트에서도 병렬적인 작업이 필요합니다.
지연된 함수의 평가
명령을 요청하면 1초가 소요되는 IO작업이 있다고 한다면, 해당 작업을 순차적으로 진행하며 go 함수를 진행할 것입니다.
const delay1000 = (a) => newPromise((resolve) => setTimeout(() => resolve(a), 1000)) // 1초가 필요한 비동기 작업
go(
[1, 2, 3, 4, 5],
L.map((a) => delay1000(a * a)),
L.filter((a) => a % 2),
reduce((a, b) => a + b),
console.log,
)
// 35
// default: 5016.022216796875 ms
지연된 함수를 병렬적으로 평가
C.reduce를 작성하여 전달받은 iter를 전개 연산자로 전달하여 줍니다. iter가 생략된 경우에는 acc가 iter이기 때문에 이 부분을 처리하여 전달합니다. 그저 전개 연산자로 전달했을 뿐인데, 모든 작업이 병렬적으로 처리되었습니다.
const C = {};
C.reduce = curry((f, acc, iter) => iter ? ruduce(f, acc, [...iter]): reduce(f, [...acc]))
const delay1000 = a => newPromise(resolve => setTimeout(() => resolve(a), 1000)) // 1초가 필요한 비동기 작업
go(
[1, 2, 3, 4, 5],
L.map(a => delay1000(a * a)),
L.filter(a => a % 2),
reduce((a, b) => a + b)
console.log
)
// 35
// default: 1005.98291015625 ms
… 전개연산자의 원리
제너레이터 함수는 next()을 진행할 때마다 yield를 한 번씩 진행하지만, 전개 연산자로 iter을 호출할 경우 남아있는 yield를 한 번에 호출하는 것을 볼 수 있습니다.
function* f() {
yield console.log(1)
yield console.log(2)
yield console.log(3)
}
const iter = f() // 아무일도 일어나지 않음
iter.next() // 콘솔이 1 찍힘
;[...iter] // 콘솔에 2, 3 찍힘
병렬적 평가에서 nop 체크하기
filter 함수에서 비동기 작업의 경우 조건이 부합하지 않는 값은 Promise.reject을 통해 자연스럽게 흘려보내도록 설계하였습니다. 하지만 이때 임의로 만든 nop이라는 구분자로 실제 에러인지, 의도한 상황인지 구별하도록 하였습니다. 이 부분에서 reject에 대해 catch하지 않아 Uncaught
에러가 발생합니다.
...
go(
[1, 2, 3, 4, 5],
L.map(a => delay1000(a * a)),
L.filter(a => a % 2),
L.map(a => delay1000(a * a)),
reduce((a, b) => a + b)
console.log
)
// 707
// Uncaught (in promise) Symbol(nop)
Promise.reject의 대한 catch
Promise.reject의 catch처리는 reduce나 take에서 처리할 것이기 때문에 reject의 catch에 아무것도 하지 않는 function() {}
함수를 할당합니다.
const C = {};
C.reduce = curry((f, acc, iter) => {
const iter2 = iter ? [...iter] : [...acc];
iter2.forEach(a => a.catch(function() {}));
// iter2 = iter.map(a => a.catch(function() {})); 이렇게 처리한다면 추후 catch 불가능!
return iter ? reduce(f, acc, iter2) : reduce(f, iter2);
});
...
코드 개선
아무것도 하지 않는 함수는 자주 사용됨으로 noop이란 이름으로 선언해 두겠습니다. reject을 catch 해주는 부분도 catchNoop이라는 이름으로 선언하여 밖으로 꺼내어 줍니다.
const C = {};
function noop() {}
const catchNoop = arr => (
arr.forEach(a => (a instanceof Promise ? a.catch(noop) : a)), arr
);
C.reduce = curry((f, acc, iter) => {
const iter2 = catchNoop(iter ? [...iter] : [...acc]);
return iter ? reduce(f, acc, iter2) : reduce(f, iter2);
});
...
C.take
reduce와 같이 결과를 만들어내는 take함수도 병렬적으로 평가할 수 있도록 catchNoop을 이용하여 작성합니다.
...
C.take = curry((l, iter) => take(l, catchNoop([...iter])));
...
C.map, C.filter
C.reduce와 C.take은 전체 작업을 모두 병렬적으로 처리하게 됩니다. 하나의 함수에서만 병렬적으로 처리해야 하는 경우도 있기 때문에 C.map과 C.filter를 작성해 보겠습니다. C.take를 이용한다면 쉽게 작성할 수 있습니다.
...
C.takeAll = C.take(Infinity);
C.map = curry(pipe(L.map, C.takeAll));
C.filter = curry(pipe(L.map, C.takeAll));