2-7. 컬렉션 중심 프로그래밍 - filter의 다형성 높이기, map과 filter 리팩토링, gen 함수
— Functional programming — 1 min read
프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 정리한 내용입니다.
# 문제점
보조함수가 Promise일 때, coll값으로 배열이 들어오면 정상동작하지만, 객체가 들어오면 루프를 1회 돌고 종료되는 문제가 있다
1log(map(a => Promise.resolve(a + 100), [1, 2, 3]))2// [101, 102, 103]3log(map(a => Promise.resolve(a + 100), {a:1, b:2, c:3}))4// { a: 101 } -> 루프 1회 돌고 종료되는 문제
객체형태로 들어온 coll을 이터러블로 만들어주는 "collIter 함수에 문제가 있는 것 같다"
# 해결과정
1. collIter()
리팩토링
문제의 보다 직접적인 원인을 찾기위해, coll이 Plain Object인지를 보기 보단, [Symbol.iterator]
가 구현되어있는지를 판별기준으로 보자
1const collIter = coll =>2 coll[Symbol.iterator] ?3 coll[Symbol.iterator]() : 4 valuesIter(coll)5 6log(map(a => Promise.resolve(a + 100), {a:1, b:2, c:3}))7// { a: 101 } -> 루프 1회 돌고 종료되는 문제
여전히 보조함수가 Promise인 경우엔 for...of가 1번 동작하고 종료되어 버리는 현상을 볼 수 있다
-> coll이 객체인지가 아니라, valuesIter를 통과한 반환값이 문제라는 걸 알 수 있다
# 배열 역시 제너레이터로 만든 객체이면 오작동한다
1var obj = {a:1,b:2,c:3}2var objIterG = valuesIter(obj)3objIterG // valuesIter {<suspended>}4
5var arr = [1,2,3]6var arrIterG = valuesIter(arr);7arrIterG // valuesIter {<suspended>}8
9reduce((a, b) => Promise.resolve(a + b), arrIterG).then(console.log) 10// 3 --> 오작동11reduce((a, b) => Promise.resolve(a + b), objIterG).then(console.log)12// 3 --> 오작동
2. 문제는 제너레이터로 만든 이터러블 객체
자세한 Debugging 과정 -> Generator로 만든 이터러블과 for...of가 만났을 때
- Array Iterator
- Generator Iterator
- 일반 이터러블객체의 iterator에는
.return()
메소드가 없지만 - 제너레이터로 만든 이터러블의 iterator에는
.return()
메소드가 있다 - for...of는 내부적으로 루프가 종료되었을 때, 받아둔 iterator에 .return()메소드가 있다면 이를 실행시켜 이터러블을 종료시킨다
return() terminates the generator
close iterators:return()
performs a return at the location of the yield that led to the last suspension of the generator.
3. gen()
: 제너레이터를 위임해주는 래퍼함수
- 제너레이터로 만든 이터러블의 return 메서드 감추기위해 객체로 감싸준다
- 제너레이터로 만든 이터러블 next메서드 실행을 gen함수에게 위임한다
next: () => iter.next()
1/*2 g : 제너레이터3 v : 제너레이터에서 받을 obj4*/5function gen(g) {6 return function(v) {7 const iter = g(v)8 return { next: () => iter.next(), [Symbol.iterator]() { return this } }9 }10}
4. 제너레이터 함수 gen으로 감싸주기
map에서 사용하는 entriesIter의 제너레이터를 gen으로 감싸주면 정상작동한다
제너레이터로 만든 이터러블의 return 메소드가 감싸져서 for...of 루프가 종료될 때 실행되지 않기 때문
1const valuesIter = gen(function *(obj) {2 for (const k in obj) yield obj[k]3})4
5const entriesIter = gen(function *(obj) {6 for (const k in obj) yield [k, obj[k]]7})8
9const reverseIter = gen(function *(arr) {10 const l = arr.length11 while(l--) yield arr[l]12})13
14log(map(a => Promise.resolve(a + 10), {a:1, b:2, c:3}, [])) 15// {a: 11, b: 12, c: 13} -> 정상작동
# reduce, filter 리팩토링
hasIter()
: coll을 판단하는 기준을 "Plain Object인지 ->[Symbol.iterator]
가 구현되있는지" 로 바꾸기baseMF()
: map, filter를 만들 때의 중복코드 제거
1. hasIter()
: 이터레이 터가 있는지 여부를 체크
작은함수의 독립. 함수들의 이터레이터 있는지 여부를 체크해주는 중복코드 제거
-> 코드가 간결해질뿐만아니라, 의미가 명확해져 가독성도 좋아진다
1const hasIter = coll => !!(coll[Symbol.iterator]);2
3const map = (f, coll) => 4 hasIter(coll) ?5 reduce((res, a) => then2(f(a), b => push(res, b)), coll, []) :6 reduce((res, [k, a]) => then2(f(a), b => set(res, k, b)), entriesIter(coll), {})7
8const filter = (f, coll) =>9 hasIter(coll) ?10 reduce((res, a) => then2(f(a), b => b ? push(res, a) : res), coll, []) :
2. baseMF()
: map, filter를 만들 베이스
map과 filter함수 코드의 다른부분은 보조함수밖에 없다. 보조함수를 제외한 나머지 코드를 baseMF 함수로 독립시켜 주자
- (f1, f2): map을 만들기위한 보조함수 (구조)
- (f, coll): 무엇으로 mapping, filter 할것인지. 다음 컨택스트에서 사용자가 전달할 조건함수
1var baseMF = (f1, f2) => (f, coll) =>2 hasIter(coll) ?3 reduce(f1(f), coll, []) :4 reduce(f2(f), entriesIter(coll), {})5
6var filter = baseMF(7 f => (res, a) => then2(f(a), b => b ? push(res, a) : res),8 f => (res, [k, a]) => then2(f(a), b => b ? set(res, k, a) : res)9)10
11var map = baseMF(12 f => (res, a) => then2(f(a), b => push(res, b)),13 f => (res, [k, a]) => then2(f(a), b => set(res, k, b))14)