-
[Functional] range, take, 느긋한 L.range, L.take, L.reduceprograming/Language 2019. 7. 22. 16:40
안녕하세요, Einere입니다.
(ADblock을 꺼주시면 감사하겠습니다.)
2019/06/25 - [programing/JavaScript] - [Functional] 평가와 일급, 고차함수
2019/07/15 - [programing/JavaScript] - [Functional] 순회와 이터러블
2019/07/15 - [programing/JavaScript] - [Functional] 제네레이터와 이터러블
2019/07/15 - [programing/JavaScript] - [Functional] map, filter, reduce
2019/07/18 - [programing/JavaScript] - [Functional] 코드를 값으로 다루어 표현력 높이기
range
const range = length => { let i = -1; let result = []; while(++i < length) { res.push(i); } return res; }; console.log(range(3)); // [0, 1, 2] 출력
지정한 범위만큼 배열을 생성하는 함수입니다.
느긋한 L.range
const L = {}; L.range = function *(length) { let i = -1; let result = []; while(++i < length) { yield i; } }; const list = L.range(3); for(const e of list) console.log(e);
지정한 범위만큼 배열을 미리 생성하지 않고, 평가할 때 동적으로 생성하는 함수입니다.
take
const take = (limit, iterable) => { let result = []; for(const e of iterable) { result.push(e); if(result.length === limit) return result; } return result; };
iterable에서 limit개수만큼 뽑아내어 반환하는 함수입니다.
const find1 = (f, iter) => L.go( iter, L.filter(val => f(val)), // 인자로 받은 f를 모든 요소에 적용시킨다 L.take(1), // 그 중 첫번째, 한 요소만 추출한다 ([user]) => user // 반환 요소가 배열이기 때문에, 비구조화 할당을 통해 요소를 추출한다 ); const find2 = (f, iter) => L.go( iter, L.filter(val => f(val)), // 인자로 받은 f를 모든 요소에 적용시킨다 L.take(Infinity), // 모든 요소를 추출한다 ([user]) => user // 반환 요소가 배열이기 때문에, 비구조화 할당을 통해 요소를 추출한다 ); console.log(find1(user => user.age > 30, users)); // 유저들 중, 나이가 30세 초과인 첫번째 유저를 출력한다 console.log(find2(user => user.age > 30, users)); // 유저들 중, 나이가 30세 초과인 모든 유저를 출력한다
위 코드와 같이
take
함수를 적용할 수 있습니다.만약, 몇개를 추출해야 될 지 모르는 상황이라면, 인자로
Infinity
를 주면 됩니다.L.take
비동기 상황(인자가 promise인 경우)를 대비해, 다음과 같이 수정할 수 있습니다.
L.nop = Symbol("nop"); L.take = L.curry(async (limit, iterable) => { let result = []; for (const e of iterable) { // result.push(await e); try { const res = await e; result.push(res); } catch (err) { if (err === L.nop) continue; else return Promise.reject(err); } if (result.length === limit) return result; } return result; });
여기서
L.nop
은L.filter
에서 사용됩니다.L.reduce
L.reduce = L.curry(async (f, acc, iterator) => { if (!iterator) { iterator = acc[Symbol.iterator](); acc = await iterator.next().value; } else { iterator = iterator[Symbol.iterator](); } for (const e of iterator) { try { acc = await f(acc, await e); } catch (err) { if (err !== L.nop) return Promise.reject(err); } } return acc; });
iterator
가 인자로 입력되지 않은경우acc
에서 뽑아내고, 입력되었다면iterator
에서 뽑아냅니다. (well formed iterable이기 때문에 가능합니다.)그리고
iterator
의 각 요소를 await하면서 순회합니다. (요소가 promise의 instance일 수 있기 때문입니다.) await한 값을 인자로 받은f
에 넣어줍니다.f
에서 promise를 리턴할 수 있기 때문에, 또 await로 결과값을 추출합니다.만약 이 과정에서 reject된 값이 있다면 catch문에 걸리게 되며,
err
를 비교하여 nop인 경우 아무짓도 하지 않으며(continue와 동일), nop을 제외한 오류라면 reject promise를 throw하듯이 return합니다.그런데 위 코드는 순회할
iterator
의 첫번째 요소를 포함하여, nop이 여러개인 경우에는 정상적으로 작동하지 않습니다.const {L, C} = require("./myUtils.js"); L.go( [1, 2, 3, 4, 5, 6, 7, 8, 9], L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))), L.filter(val => Promise.resolve(val > 6)), C.reduce((acc, val) => { console.log('reduce...', acc, val); return acc + val }), console.log, ).catch(console.log); /* 출력 Object [Generator] {} */
위 코드에서 제네레이터가 출력되었다는 것은,
L.reduce
가 아무런 동작을 하지 않았기 때문입니다. 이 모든 원인은 순회할 첫번째 요소가 nop으로 인해 만들어진 promise이기 때문입니다.이러한 현상을 해결하기 위해, 순회할
iterator
의 첫번째 요소부터 연속적으로 nop을 최대한 제거하는 코드는 다음과 같습니다.L.reduce = L.curry(async (f, acc, iterator) => { if (!iterator) { iterator = acc[Symbol.iterator](); // 순회할 iterator의 앞쪽부터 nop을 최대한 제거한다 acc = await async function dequeueNop(acc, iterator){ try{ acc = await iterator.next().value; } catch(err) { if(err !== L.nop) return Promise.reject(err); if(err === L.nop) return dequeueNop(acc, iterator); } return acc; }(acc, iterator); } else { iterator = iterator[Symbol.iterator](); } for (const e of iterator) { try { acc = await f(acc, await e); } catch (err) { if (err !== L.nop) return Promise.reject(err); } } return acc; /*return L.branch(acc, function recurReduce(acc) { let e; while (!(e = iterator.next()).done) { acc = L.branch2(acc, e.value, f); if (acc instanceof Promise) return acc.then(recurReduce); } return acc; });*/ });
재귀함수인
dequeueNop
을 이용하여, 정상적인 값이라면 바로acc
를 반환하며, nop이라면 재귀호출을, 그 외의 에러라면Promise.reject(err)
를 반환합니다.다음은 개선된
L.reduce
를 활용한 예제입니다.const {L, C} = require("./myUtils.js"); L.go( [1, 2, 3, 4, 5, 6, 7, 8, 9], L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))), L.filter(val => Promise.resolve(val > 6)), L.reduce((acc, val) => { console.log('reduce...', acc, val); return acc + val }), console.log, ).catch(console.log); /* 출력 reduce... 7 8 reduce... 15 9 24 */ L.go( [1, 2, 3, 4, 5, 6, 7, 8, 9], L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))), L.filter(val => Promise.resolve(!(val % 2))), L.reduce((acc, val) => { console.log('reduce...', acc, val); return acc + val }), console.log, ).catch(console.log); /* 출력 reduce... 2 4 reduce... 6 6 reduce... 12 8 20 */
참고
마이클 포거스, 『함수형 자바스크립트 : 새롭고 올바른 자바스크립트 프로그래밍 기법』, 한빛미디어(2014).
유인동, 인프런 - 함수형 프로그래밍과 JavaScript ES6+
'programing > Language' 카테고리의 다른 글
[JS] promise와 async await의 차이 (0) 2019.07.23 [Functional] 지연 평가와 L.map, L.filter (0) 2019.07.22 [JS] clean code (0) 2019.07.20 [Functional] 코드를 값으로 다루어 표현력 높이기 (0) 2019.07.18 [JS] Object.prototype.function의 호출객체를 얻는 방법 (0) 2019.07.17 댓글