range와 느긋한 L.range
숫자를 받아 숫자 크기의 배열을 return하는 함수.
//range const range = l => { let i =0; let res = []; while(i < l) { res.push(i); i++; } return res; } const add = (a,b) => a+b; console.log(range(5)); //[ 0, 1, 2, 3, 4 ] var list = range(4); console.log(reduce(add, list)); //6 //느긋한 range const L = {}; L.range = function*(l) { let i =0; let res = []; while(i < l) { yield i; i++; } } console.log(L.range(5)); //[ 0, 1, 2, 3, 4 ] var list = L.range(4); console.log(reduce(add, list)); //6
- list를 출력하면 range는 배열을, L.range는 이터레이터를 출력함.
- L.range는 평가가 완벽히 되지 않은 상태를 유지(array를 만들지 않음)하다 유의미한 결과값이 필요할 때 평가함. 예를 들어 take나 reduce의 함수를 실행할 때 평가를 시작함.
- 콘솔 값이 같은 이유는 reduce가 이터러블을 받기 때문임.
성능 테스트
function test(name, time, f) { console.time(name); while(time--) f(); console.timeEnd(name); } test('L.range',10,()=> reduce(add, L.range(1000000))); //L.range: 251ms test('range',10,()=> reduce(add, range(1000000))); //range: 276ms
- range는 1) array를 만들고, 2)이터레이터를 만들고 3)next로 순회.
- L.range는 1)이터레이터를 만들고, 2)이 이터러블은 자기자신을 return하는 이터러블이고, 3) 순회.
- L.range가 좀 더 효율적임.
take
지연성이 가진 효율성을 알아보자.
const take = (l, iter) => { let res = []; for(const a of iter) { res.push(a); if(res.length === l) return res; } } console.log(take(5, range(100))); //[ 0, 1, 2, 3, 4 ] console.log(take(5, L.range(100))); //같은 결과값 이지만 range 크기가 커질수록 효율적
- 지연성을 가지는 값을 이터레이터로 만들게 되면 이터러블 프로토콜을 따르는 함수와의 조합성이 높음.
- 또한 range는 100개의 배열을 만들고 5개를 자르는 방식인 반면, L.range는 범위에서 5개의 값만을 만들기 때문에 효율적임.
- 심지어 L.range의 범위로 Infinity를 넣어도 가능함.
Lmap
이터러블, 이터레이터 프로토콜을 따르면서 지연성을 가지는 map을 구현해보자.
Lmap = function *(f, iter) { for(const a of iter) yield f(a); }; var it = Lmap(a => a + 10, [1,2,3]);//아직 평가되지 않음. console.log(it.next()); //{ value: 11, done: false } console.log([...it]); //[12, 13]
- 지연성을 가지고 (평가를 미루고) 평가순서를 달리 조작할 수 있는 이터레이터를 반환하는 제너레이터 함수.
- Lmap 자체는 새로운 array를 만들지 않고 값마다 순회하며 yield 통해 함수에 적용된 값을 이터레이터의 next를 실행할 때마다 하나씩 전달하는 구조.
- 내가 원하는 방법으로 평가할 수 있음. ex)
[it.next().value]
,[...it]
Lfilter
Lfilter = function *(f, iter) { for(const a of iter) if(f(a)) yield f(a); }; const it = Lfilter( a => a%2, [1,2,3,4]); console.log(it.next()); //{ value: 1, done: false }
- yield가 배열 크기만큼 실행되는 것이 아닌 조건에 따라 먼저 필터되고 실행됨.
Array.prototype.join 보다 다형성이 높은 join 함수
함수형 프로그래밍에서 join은 array가 아니어도 적용할 수 있음.
const join = curry((sep = ',', iter) => reduce((a,b) => `${a}${sep}${b}`, iter)); function *a() { yield 10; yield 11; yield 12; yield 13; } console.log(a().join('-')); //error console.log(join('-', a())); //10-11-12-13
- reduce 계열의 함수임.
take, find
const users = [ {age:32}, {age:31}, {age:37}, {age:28}, {age:25}, {age:32}, {age:31} ] const find = (f, iter) => go( iter, Lfilter(f), take(1), ([a]) => a ) console.log(find(u => u.age < 30, users)); //{ age: 28 }
- find함수는 take를 이용해 결론을 만듦.
- filter일 경우 모든 경우를 순회하고 take를 처리하기 때문에 Lfilter를 사용함.
L.flatten, flatten
값을 모두 펼쳐 하나의 배열을 만드는 함수. 지연적으로 동작함.
console.log([...[1,2],3,4,...[5,6],...[7,8,9]]) //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] //원리 const isIterable = a => a && a[Symbol.iterator]; //null 방지 차원에서 &&사용. Lflatten = function *(iter) { for(const a of iter) { if(isIterable(a)) for (const b of a) yield b; //추가 depts로 a 안에 있는 모든 b를 yield함. else yield a; } }; var it = Lflatten([[1,2],3,4,[5,6],[7,8,9]]); console.log(it.next()); //{ value: 1, done: false } console.log(take(6, Lflatten([[1,2],3,4,[5,6],[7,8,9]]))); //[ 1, 2, 3, 4, 5, 6 ]
L.flatMap, flatMap
값을 모두 펼쳐 하나의 배열을 만드는 함수. 지연적으로 동작함.
LflatMap = curry(pipe(Lmap, Lflatten)); var it = LflatMap(map(a => a*a), [[1,2],[3,4],[5,6],[7,8,9]]); console.log([...it]); //[ 1, 4, 9, 16, 25, 36, 49, 64, 81 ]
함수형 프로그래밍 라이브러리
export function* range(start = 0, stop = start, step = 1) { if (arguments.length === 1) start = 0; if (arguments.length < 3 && start > stop) step *= -1; if (start < stop) { while (start < stop) { yield start; start += step; } } else { while (start > stop) { yield start; start += step; } } } export function map(f) { return function* (iter) { for (const a of iter) yield f(a); } } export function filter(f) { return function* (iter) { for (const a of iter) if (f(a)) yield a; } } export function take(limit) { return function* (iter) { for (const a of iter) { yield a; if (--limit === 0) break; } } } export function reduce(f) { return function (acc, iter) { if (!iter) acc = (iter = acc[Symbol.iterator]()).next().value; for (const a of iter) acc = f(acc, a); return acc; } } export function each(f) { return function (iter) { for (const a of iter) f(a); return iter; } } export function go(arg, ...fs) { return reduce((arg, f) => f(arg))(arg, fs); } export const head = ([a]) => a; export const find = (f) => (iter) => head(filter(f)(iter)); export function inc(parent, k) { parent[k] ? parent[k]++ : (parent[k] = 1); return parent; } export const countBy = (f) => (iter) => reduce((counts, a) => inc(counts, f(a)))({}, iter); export const identity = a => a; export const count = countBy(identity); export const groupBy = (f) => (iter) => reduce( (group, a, k = f(a)) => ((group[k] = (group[k] || [])).push(a), group) )({}, iter); export function* entries(obj) { for (const k in obj) yield [k, obj[k]]; } export function* values(obj) { for (const k in obj) yield obj[k]; } export const isFlatable = a => a != null && !!a[Symbol.iterator] && typeof a !== 'string'; export function* flat(iter) { for (const a of iter) isFlatable(a) ? yield* a : yield a; } export function zip(a) { return function* (b) { a = a[Symbol.iterator](); b = b[Symbol.iterator](); while (true) { const { value, done } = a.next(); const { value: value2, done: done2 } = b.next(); if (done && done2) break; yield [value, value2]; } } } export function concat(...args) { return flat(args);
- 고차함수와 보조함수를 데이터에 적절하게 조합함
- 어떤 일을 추상적으로 사고하기 용이함.
- 데이터가 자주 바뀌는 구조에 쓰기 좋음.
- 함수형 프로그래밍은 속도보단 안정성을 추구하는 방식임.