Skip to content
Underbleu
GithubLinkedin

3-1. map, filter 계열 함수 만들기

Functional programming1 min read

프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 정리한 내용입니다.

# Javascript Basic

Map과 Set

ES6에 새로 도입된 데이터구조

  • Map - 객체의 단점을 해결한 데이터 구조

    • size 프로퍼티 - key-value 가 몇 개 인지 확인 가능
    • 객체의 key로 다양한 타입사용 가능 (숫자, 객체...)
    • 프로퍼티의 순서를 보장
  • Set - 배열과 비슷하지만, 중복을 허용않는 데이터 구조

1const m = new Map()
2m.set(1, 'a')
3m.set({}, 'b')
4m.size // 2
5log(m) // Map(2) {1 => "a", {…} => "b"}
6
7const s = new Set()
8s.add('a')
9s.add('b')
10s.add('a') // --> 중복데이터. 무시
11s.size // 2
12log(s) // {"a", "b"}

Iterable computed data

ES6의 data-structure (Arrays, Typed Arrays, Maps, Sets)는 well-formed 이터러블을 리턴해주는 3가지 메서드를 가지고 있다

  • keys() - key를 담고있는 Iterator object 리턴
  • values() - value를 담고있는 Iterator object 리턴
  • entries() - [key, value]를 담고있는 Iterator object 리턴
    • 배열의 경우 [index, element]
    • Set의 경우 [key, key]
1[][Symbol.iterator]() // Array Iterator {}
2[].values() // Array Iterator {}
3
4const arr = ['a', 'b', 'c']
5const arrEntries = arr.entries()
6
7arrEntries === arrEntries[Symbol.iterator]()
8// true -> well-formed 이터러블
9
10arrEntries.next().value
11// [0, 'a'] -> 1. Remember
12
13for (const pair of arr.entries()) log(pair)
14// [1, 'b'] -> 2. Continue with same Iterator
15// [2, 'c']

# baseMF 리팩토링

리팩토링 전

1const baseMF = (f1, f2) => (f, coll) =>
2 hasIter(coll)
3 ? reduce(f1(f), coll, [])
4 : reduce(f2(f), entriesIter(coll), {});
5
6const map = baseMF(
7 f => (res, a) => then2(f(a), b => push(res, b)),
8 f => (res, [k, a]) => then2(f(a), b => set(res, k, b))
9)
10
11const filter = baseMF(
12 f => (res, a) => then2(f(a), b => b ? push(res, a) : res),
13 f => (res, [k, a]) => then2(f(a), b => b ? set(res, k, a) : res),
14)

리팩토링 후

1const baseMF = (f1, f2) => (f, coll) =>
2 hasIter(coll) ?
3 reduce((res, a) => then2(f(a), b => f1(res, a, b)), coll, []) :
4 reduce((res, [k, a]) => then2(f(a), b => f2(res, k, a, b)), entriesIter(coll), {})
5
6const map = baseMF(
7 (res, a, b) => push(res, b),
8 (res, k, a, b) => set(res, k, b))
9
10const filter = baseMF(
11 (res, a, b) => b ? push(res, a) : res,
12 (res, k, a, b) => b ? set(res, k, a) : res)

# map계열 함수

values() - 객체의 value를 추출하는 함수

Object.values() 메서드에선 지원하지 못하는 Set, Map등의 값도 받을 수 있도록 다형성을 높혀보자

  • Object.values() (ES8)
    • 객체의 속성중, 부수속성이 "enumerable: true"한 값을 배열로 리턴
    • 내부적으로 for...in처럼 key값을 이용해 순회하기 때문에 Set, Map에 사용불가

_1. map을 활용한 values()함수 만들기

  • 기존의 map()함수를 이용하면 Set은 잘 동작하지만, Map, Plain Object는 대응안됨
  • Map은 디폴트 [Symbol.iterator]로 entries를 사용하기 때문에 값만 추출되지 않고 key-value쌍으로 출력된다
  • Plain Object 역시 key-value쌍으로 출력된다
1const values1 = coll => map(a => a, coll)
2
3values1(new Set([1, 2, 3, 4])) // [1, 2, 3, 4]
4values1(new Map([['a', 1], ['b', 2]])) // [["a", 1], ["b", 2]]
5values1({a:1, b:2})) // {a: 1, b: 2}
1Map.prototype[Symbol.iterator] // ƒ entries() { ... }
2Set.prototype[Symbol.iterator] // ƒ values() { ... }

_2. Map 대응

Map이 들어오면 [Symbol.iterator]로 values를 사용하도록 설정

1const values2 = coll =>
2 coll instanceof Map ?
3 map(a => a, coll.values()) :
4 map(a => a, coll);
5
6values2(new Map([['a', 1], ['b', 2]])) // [1, 2]

_3. Plain Object 대응

Map이외의 값은 collIter를 통과시켜 valuesIter로 값만 추출될 수 있도록 한다

1const collIter = coll =>
2 hasIter(coll) ? coll[Symbol.iterator]() : valuesIter(coll)
3
4const values3 = coll =>
5 coll instanceof Map ?
6 map(a => a, coll.values()) :
7 map(a => a, collIter(coll));
8
9values3({ a: 1, b: 2 }) // [1, 2]

_4. 완성

values()는 단순히 값을 추출하는 역할을 넘어, 값을 평가하는 확장성까지 있는 함수이다

1const values = coll =>
2 map(identity, coll instanceof Map ? coll.values() : collIter(coll))
3
4values(function *() {
5 yield 1;
6 yield 2;
7 yield 3;
8} ())
9// [1, 2, 3] -> 값을 평가하는 확장성

entries() - 객체의 [key, value]를 추출하는 함수

1var entries = coll =>
2 map(identity, hasIter(coll) ? coll.entries() : entriesIter(coll))
3
4entries(new Set(['a', 'b']))
5// [['a', 'a'], ['b', 'b']]
6
7entries(new Map([['a', 1], ['b', 2]]))
8// [['a', 1], ['b', 2]]

# filter계열 함수

reject() - falsy한 값을 추출하는 함수

  • reject는 falsy한 값을
  • filter는 truthy한 값을 추출

_1. filter를 활용하여 falsy한 값을 뽑는 함수 만들어보기

1const reject = (f, coll) => filter(a => !f(a), coll);
2
3console.log(filter(a => a % 2, [1, 2, 3, 4])); // [1, 3]
4console.log(reject(a => a % 2, [1, 2, 3, 4])); // [2, 4]

_2. 보조함수에서 프로미스를 리턴했을 때

Promise 값을 부정할 수 없는 문제
-> 비동기가 일어날 수 있는 곳이라면 "하나의 값에, 하나의 평가"만 이루어지도록하자

  • not() - 보조함수 f(a)의 리턴값을 then함수로 감싸서, Promise이던 아니던 모두 부정할 수 있게해줌
  • filter가 다형성을 지원하면, reject역시 다형성을 지원하는 범위가 늘어난다
1const not = a => then2(a, a => !a);
2const reject = (f, coll) => filter(a => not(f(a)), coll);
3
4filter(a => Promise.resolve(a % 2), [1, 2, 3, 4]) // [1, 3]
5reject(a => Promise.resolve(a % 2), [1, 2, 3, 4]) // [2, 4]

compact() - truthy한 값만 남게하는 함수

1const compact = coll => filter(identity, coll);
2
3compact([ 0, false, {}, 10, null ]) // [{ }, 10]

# 의존성이 없는 함수형프로그래밍

형을 정확히 다루면서, 계열중심의 함수(map, filter..)들을 사용하여 함수형 프로그래밍을 하면

  • 계층구조가 있는 함수라도 서로 절대 의존성이 없다는 보장을 할 수 있다
  • 즉, 위에 있는 부모격의 함수가 수정되었을때, 자식 함수가 오작동할 리가 없다는 보장이 된다
  • 반면에, 객체지향에선 상위 객체를 고치면 하위의 객체들은 거의 영향을 받는다