Search
🏛️

[FP & JS ES6+] 5. 비동기: 동시성 프로그래밍 (2) - 지연평가 + Promise, 병렬적 평가

[FP & JS ES6+] 6. 비동기: 동시성 프로그래밍 (3) - async/await, Q&A
Javascript
Functional Programming
Study
2023/10/0614:19
[FP & JS ES6+] 6. 비동기: 동시성 프로그래밍 (3) - async/await, Q&A
Javascript
Functional Programming
Study
2023/10/0614:19

1. 지연평가 + Promise

1.1. L.map, map, take

// go1 const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a); // L.map L.map = curry(function* (f, iter) { for (const a of iter) { yield go1(a, f); } }); // take export 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); if (res.length === l) return res; return recur(); }); } res.push(a); if (res.length === l) return res; } return res; } () }); // take all export const takeAll = take(Infinity); // map export const map = curry(pipe(L.map, takeAll));
JavaScript
복사
적용 코드
import {log, go, L, take, map, takeAll} from '../0_common/fx.js'; go( [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)], map(a => Promise.resolve(a + 10)), take(2), log); go( [1, 2, 3], map(a => Promise.resolve(a + 10)), takeAll, log);
JavaScript
복사

1.2. Kleisli Composition - L.filter, filter, nop, take

L.filter & take에 promise 적용
비동기 동시성과 지연평가가 모두 가능한 코드 구현
const nop = Symbol('nop'); // go const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a); // L.map L.map = curry(function* (f, iter) { for (const a of iter) { yield go1(a, f); } }); // L.filter 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 export 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); if (res.length === l) return res; return recur(); }).catch(e => e === nop ? recur() : Promise.reject(e)); } res.push(a); if (res.length === l) return res; } return res; }() });
JavaScript
복사
적용 코드
import {go, L, log, take} from '../0_common/fx.js'; // Kleisli Composition - L.filter, filter, nop, take go([1, 2, 3, 4, 5, 6], L.map(a => Promise.resolve(a * a)), L.filter(a => Promise.resolve(a % 2)), L.map(a => Promise.resolve(a * a)), take(2), log ); go([1, 2, 3, 4, 5, 6], L.map(a => a * a), L.filter(a => a % 2), L.map(a => a * a), take(2), log );
JavaScript
복사

1.3. reduce - nop 지원

reduce에 promise 적용
before
export const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value } return go1(acc, function recur(acc) { let cur; while (!(cur = iter.next()).done) { acc = f(acc, a); if (acc instanceof Promise) return acc.then(recur); } return acc; }); });
JavaScript
복사
after
const reduceF = (acc, a, f) => a instanceof Promise ? a.then(a => f(acc, a), e => e === nop ? acc : Promise.reject(e)) : f(acc, a); export const reduce = curry((f, acc, iter) => { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value } 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; }); });
JavaScript
복사
iter가 없는 경우 함수 표현식으로 리팩토링
const reduceF = (acc, a, f) => a instanceof Promise ? a.then(a => f(acc, a), e => e === nop ? acc : Promise.reject(e)) : f(acc, a); const head = iter => go1(take(1, iter), ([h]) => h); export const reduce = curry((f, acc, iter) => { if (!iter) { return reduce(f, head(iter = acc[Symbol.iterator]()), iter); } 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; }); });
JavaScript
복사
적용 코드
import {go, L, log, reduce, takeAll} from '../0_common/fx.js'; // reduce에서 nop 지원 const add = (a, b) => a + b; go([1, 2, 3, 4], L.map(a => Promise.resolve(a * a)), L.filter(a => Promise.resolve(a % 2)), reduce(add), log // 25 );
JavaScript
복사

1.4. 지연평가 + Promise의 효율성

takeAll로 대략 16초가 걸릴 로직도 take(2)만 할경우 4초 내로 로직 수행 가능
import {go, L, log, take, takeAll, map, filter} from '../0_common/fx.js'; go([1, 2, 3, 4, 5, 6, 7, 8], L.map(a => { log(a); return new Promise(resolve => setTimeout(() => resolve(a * a), 1000)); }), L.filter(a => { log(a); return new Promise(resolve => setTimeout(() => resolve(a % 2), 1000)); }), takeAll, log ); go([1, 2, 3, 4, 5, 6, 7, 8], L.map(a => { log(a); return new Promise(resolve => setTimeout(() => resolve(a * a), 1000)); }), L.filter(a => { log(a); return new Promise(resolve => setTimeout(() => resolve(a % 2), 1000)); }), take(2), log );
JavaScript
복사

2. 지연된 함수열을 병렬적으로 평가하기

2.1. C.reduce

비동기성을 고려하지 않고, 모든 배열을 평가해 reduce로 넘김
적용 코드
L.map과 L.filter를 사용하므로 go함수 내부 reduce에서 함수별로 하나씩 동기적으로 delay1000 함수 실행
C.reduce를 활용하면 배열을 펼쳐 한 번에 평가 후 다음 함수로 전달
import {curry, go, L, log, reduce} from '../0_common/fx.js'; // 지연된 함수열을 병렬적으로 평가하기 - C.reduce, C.take export const C = {}; C.reduce = curry((f, acc, iter) => iter ? reduce(f, acc, [...iter]) : reduce(f, [...acc])); const add = (a, b) => a + b; const delay1000 = a => new Promise(resolve => setTimeout(() => resolve(a), 1000)); go([1, 2, 3, 4, 5], L.map(a => delay1000(a * a)), L.filter(a => a % 2), // reduce(add), C.reduce(add), log );
JavaScript
복사

2.2. Promise.reject의 평가

병렬적으로 함수를 실행하다보면 비동기적으로 promise reject가 발생해 작업을 처리해줘야하는 경우가 발생한다.
처리를 따로 안하더라도 코드는 동작하나, console에 에러가 찍힘.
이를 방지하기 위해 promise reject를 catch하여 처리 진행
단, 이때 catch된 iterator는 또다시 catch하는것은 불가능하다.
적용 코드
import {curry, go, L, log, reduce} from '../0_common/fx.js'; // Promise reject 평가 export 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); }); const add = (a, b) => a + b; const delay1000 = a => new Promise(resolve => setTimeout(() => resolve(a), 1000)); go([1, 2, 3, 4, 5,6,7,8,9], L.map(a => delay1000(a * a)), L.filter(a => delay1000(a % 2)), L.map(a => delay1000(a * a)), C.reduce(add), log );
JavaScript
복사

2.3. C.take

마찬가지로 배열을 병렬 처리 하기 위해 spread 한뒤 즉시 평가 후 catchNoop에 전달
적용 코드
import {curry, go, L, log, reduce, take} from '../0_common/fx.js'; // C.take export 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 = curry((l, iter) => { return take(l, catchNoop([...iter])); }); const add = (a, b) => a + b; const delay1000 = a => new Promise(resolve => setTimeout(() => resolve(a), 1000)); go([1, 2, 3, 4, 5, 6, 7, 8, 9], L.map(a => delay1000(a * a)), L.filter(a => delay1000(a % 2)), L.map(a => delay1000(a * a)), C.take(2), C.reduce(add), log );
JavaScript
복사

2.4. C.map, C.filter

병렬적으로 즉시 평가 진행
C.map = curry(pipe(L.map, C.takeAll)); C.filter = curry(pipe(L.filter, C.takeAll)); const delay1000 = a => new Promise(resolve => setTimeout(() => resolve(a), 1000)); C.map(a => delay1000(a * a), [1, 2, 3, 4]).then(log); C.filter(a => delay1000(a % a), [1, 2, 3, 4]).then(log);
JavaScript
복사

3. 즉시, 지연, Promise, 병렬적 조합하기

3.1. 즉시 평가

모든 평가를 즉시, 엄격하게 실행
각 함수들이 종(왼쪽 → 오른쪽)으로 실행되고 완료된 이후 다음 함수로 이동함
소요시간 약 9초
const delay500 = (a, name) => new Promise(resolve => { console.log(`${name}: ${a}`); setTimeout(() => resolve(a), 500); }); console.time(''); go([1, 2, 3, 4, 5, 6, 7, 8, 9], map(a => delay500(a * a, 'map 1')), filter(a => delay500(a % 2, 'filter 2')), map(a => delay500(a + 1, 'map 3')), take(2), log, _ => console.timeEnd(''));
JavaScript
복사
출력된 결과
map 1: 1 map 1: 4 map 1: 9 map 1: 16 map 1: 25 filter 2: 1 filter 2: 0 filter 2: 1 filter 2: 0 filter 2: 1 map 3: 2 map 3: 5 map 3: 10 map 3: 17 map 3: 26 [ 2, 5 ]
JavaScript
복사

3.2. 지연 평가

모든 평가를 지연(Lazy)하게 진행
각 함수들의 평가를 횡(위 → 아래)로 실행
평가를 최소화하는 방향으로 동작
소요시간 약 4초
console.time(''); go([1, 2, 3, 4, 5], L.map(a => delay500(a * a, 'map 1')), L.filter(a => delay500(a % 2, 'filter 2')), L.map(a => delay500(a + 1, 'map 3')), take(2), log, _ => console.timeEnd(''));
JavaScript
복사
출력된 결과
map 1: 1 filter 2: 1 map 3: 2 map 1: 4 filter 2: 0 map 1: 9 filter 2: 1 map 3: 10
JavaScript
복사

3.3. 병렬적으로 평가

특정 로직을 병렬적으로 즉시 실행하게끔 적용 가능
아래 예시에서는 첫번째 map 함수를 병렬적으로 평가 진행
소요시간 약 3초
console.time(''); go([1, 2, 3, 4, 5], C.map(a => delay500(a * a, 'map 1')), L.filter(a => delay500(a % 2, 'filter 2')), L.map(a => delay500(a + 1, 'map 3')), take(2), log, _ => console.timeEnd(''));
JavaScript
복사
출력된 결과
map 1: 1 map 1: 4 map 1: 9 map 1: 16 map 1: 25 filter 2: 1 map 3: 2 filter 2: 0 filter 2: 1 map 3: 10
JavaScript
복사

3.4. 다양한 조합

console.time(''); go([1, 2, 3, 4, 5], L.map(a => delay500(a * a, 'map 1')), C.filter(a => delay500(a % 2, 'filter 2')), L.map(a => delay500(a + 1, 'map 3')), take(2), log, _ => console.timeEnd('')); map 1: 1 map 1: 4 map 1: 9 map 1: 16 map 1: 25 filter 2: 1 filter 2: 0 filter 2: 1 filter 2: 0 filter 2: 1 map 3: 2 map 3: 10
JavaScript
복사
console.time(''); go([1, 2, 3, 4, 5], L.map(a => delay500(a * a, 'map 1')), L.filter(a => delay500(a % 2, 'filter 2')), L.map(a => delay500(a + 1, 'map 3')), C.take(2), log, _ => console.timeEnd('')); map 1: 1 map 1: 4 map 1: 9 map 1: 16 map 1: 25 filter 2: 1 filter 2: 0 filter 2: 1 filter 2: 0 filter 2: 1 map 3: 2 map 3: 10 map 3: 26
JavaScript
복사