Skip to content
Underbleu
GithubLinkedin

2-7. 컬렉션 중심 프로그래밍 - filter의 다형성 높이기, map과 filter 리팩토링, gen 함수

Functional programming1 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(): 제너레이터를 위임해주는 래퍼함수

  1. 제너레이터로 만든 이터러블의 return 메서드 감추기위해 객체로 감싸준다
  2. 제너레이터로 만든 이터러블 next메서드 실행을 gen함수에게 위임한다
    next: () => iter.next()
1/*
2 g : 제너레이터
3 v : 제너레이터에서 받을 obj
4*/
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.length
11 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 리팩토링

  1. hasIter(): coll을 판단하는 기준을 "Plain Object인지 -> [Symbol.iterator]가 구현되있는지" 로 바꾸기
  2. 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)