Skip to content
Underbleu
GithubLinkedin

2-1. 컬렉션 중심 프로그래밍 - reduce, 제너레이터, 다형성

Functional programming2 min read

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

# 컬렉션 중심 프로그래밍이란 ?

자바스크립트 함수형 프로그래밍 책 250p

  • 컬렉션 중심 프로그래밍의 목표는 컬렉션을 다루는 좋은 로직의 함수 세트들을 만들어 재사용성을 극대화 시키는 데 있다
  • 함수를 조합하는 식으로 프로그래밍을 하면 코드는 간결해지고 표현력은 풍부해진다
  • 많은 데이터형을 지원하는 화려한 함수 10개보다, 적은 데이터형을 지원하며 작은 기능만을 정확히 수행하는 함수 100개가 낫다
  • 크기가 작기때문에 값의 변이 과정을 고려하지 않고도, 코드가 무슨 일을 하는지 쉽게 알 수 있다
  • 작은 함수들을 조합하여 로직을 짜게 되면, 함수 이름으로 로직을 읽기 때문에 복잡한 코드를 이해하기 쉽다

# 4가지 유형별 대표함수

각 유형의 중 추상화 레벨이 가장 높은 함수. 즉, 이 함수를 이용해 필요에따라 다양한 함수를 만들어 나갈 수 있다

  • map: 다 돌면서, 수집하기
  • filter: 다 돌면서, 거르기
  • reduce: 다 돌면서 좁히기. 접기
  • find: 돌면서 원하는 결과를 완성하면 나가기. 찾아내기
    -> 컬렉션을 다 돌지 않기 때문에 성능최적화에 유리함

# 함수형 프로그래밍의 특징

  • 적은 수의 자료구조, 많은 연산자
  • 패턴매칭: 특정한 '모양'(패턴)을 가지고 있는가를 테스트하는 것

# reduce함수 만들기

  • Array.prototype.reduce메소드는 유사배열등의 배열이 아닌 객체에서는 사용을 할 수 없다
  • [Symbol.iterator] 메서드가 구현되어 있지 않은 객체에도 사용 가능한 reduce 함수를 만들어보자
1/* f: 적용할 함수
2 * coll: 콜렉션 데이터 (돌릴 수 있는)
3 * acc: 누적값. 공백시 coll의 첫번째 요소를 초기값으로 사용. *optional */
4
5function reduce(f, coll, acc) {
6 const iter = coll[Symbol.iterator]();
7 acc = acc === undefined ? iter.next().value : acc;
8 for (const v of iter) {
9 acc = f(acc, v);
10 }
11 return acc;
12}
13
14console.log( reduce((acc, a) => acc + a, [1,2,3]) ); // 6
15console.log( reduce((acc, a) => acc + a, [1,2,3], 10) ); // 16

1. coll을 well-formed 이터러블로 만들어주기

coll이 기본적으로 이터러블이어도, [Symbol.iterator] 메서드를 한 번 시켜줘야 well-formed 이터러블로 사용할 수 있다 -> well-formed 이터러블의 장점 (feat. 피보나치수열)

1const iter = coll[Symbol.iterator]();

이터레이션 프로토콜(iteration protocol)

  • Iterable(이터러블)
    : [Symbol.iterator]라는 특정한 이름의 method가 구현되어있는 순회 가능한 자료 구조
  • Iterator (이터레이터)
    : 이터러블의 [Symbol.iterator]()가 반환한 값
    • 이터러블의 요소를 탐색하기 위한 포인터
    • next method가 구현되어 있어야 한다
  • next method
    : value, done 프로퍼티를 갖는 객체를 반환 한다
    • done: false -> 생략가능
    • done: true일 때, value 생략가능

2. acc를 optional하게 만들어주기

1acc = acc === undefined ? coll.next().value : acc;
  • undefined를 구분자로 사용하여 acc가 들어왔는지 여부를 체크한다
  • iter.next() 활용하여 첫번째 요소를 미리 꺼내 acc에 할당하고, 뒤의 요소를 순회한다
    • Iterator를 활용하여 값을 꺼내기 때문에, 코드도 깔끔하고 성능문제도 없다
    • ES5였다면 slice로 첫요소를 제외한 배열을 통채로 복사하여 coll을 재정의 해야 때문에, 배열의 크기가 클 경우 성능이슈가 있다
    1// ES5 였다면 어휴...
    2acc = acc === undefined ? coll[0] : acc;
    3coll = coll.slice(1); // coll 재정의: 첫번째 요소만 제외한 요소들 통채로 복사

# collIter(): 이터러블을 만들어주는 작은 함수 독립

reduce함수에 명령적으로 쓰여져 있던 코드를 collIter()로 독립시켜준다

  • 향후 collIter에 다형성을 높여줄 수 있는 코드를 추가할 수 있다
    -> 배열뿐만 아니라, 객체도 지원해줄 수 있도록
  • 받는 인자의 타입이 어떻던 리턴값은 Iterable로 유지할거라, 기존에 명령형으로 coll을 iterable하게 만들어주던 기능은 무너지지 않음
1const collIter = coll => coll[Symbol.iterator]();
2
3function reduce(f, coll, acc) {
4//coll = coll[Symbol.iterator](); //--- 명령형
5 coll = collIter(coll); //--- 선언형
6 // ...
7}

# 명령형 / 선언형 프로그래밍의 차이

참고: 명령형과 함수형 프로그래밍 비교

  • 명령형 프로그램은 알고리즘을 명시하고, 목표는 명시하지 않는다
    -> 알고리즘: 수행해야 하는 단계를 매우 자세히 설명하는 코드

  • 선언형 프로그램은 목표를 명시하고, 알고리즘을 명시하지 않는다
    -> 실행할 일련의 함수(목표)로 코드를 구성

    _명령형함수형
    포인트작업을 수행하는 방법(알고리즘)과 상태의 변경을 추적하는 방법원하는 정보와 필요한 변환 (=인자와 리턴값)
    상태 변경중요존재하지 않음
    실행 순서중요중요도가 낮음
    흐름 제어루프, 조건 및 함수(메서드) 호출재귀를 비롯한 함수 호출
    조작 단위클래스나 구조체의 인스턴스1급(first-class) 개체와 데이터 컬렉션인 함수

# valuesIter(): 인자의 다형성을 높여주기 위한 보조함수

  • 성능적으로 이슈가 없다면 함수를 범용적으로 만들 필요가 있다
    -> reduce()가 배열뿐만 아니라, 객체도 인자로 받을 수 있도록 만들어 주자
  • 객체는 이터러블이 아니지만 이터레이션 프로토콜을 준수하면 순회할 수 있는 이터러블 객체를 만들수 있다
    -> 제너레이터 함수로 순회 가능한(iterable) 값을 생성해주자
1// 1. 기존 객체에서 값을 하나씩 꺼내서 전달하는 함수
2function *valuesIter(obj) {
3 for(const k in obj) yield obj[k];
4}
5
6// 2. 기존 객체과 같은 크기의 배열을 새로 만드는 함수 (성능이슈**)
7function toArray(obj) {
8 return [...valuesIter(obj)]
9}

-> toArray(makeArr(300000000)) 부터 실행즉시 콜스택에러 터짐. 성능이슈 !

# 성능이슈 Tip

  • for안에서 코드를 추가하는게 아니면, 성능상 문제를 주는 코드는 거의 없다
  • 심지어 for문 안에서 if문으로 조건 체크를 만번, 이만번을 돌려도 성능 차이가 거의 없기때문에, 다형성을 지원하기위한 조건들을 함수에 많이 넣어줘도 된다
  • 이런식으로 Array.prototype.reduce가 지원하지 못하는 상황을 고려하여, 내가 직접 다형성을 지원하는 함수를 구현해볼 수 있다
  • console.time(), console.timeEnd() 으로 성능 차이 확인해볼 수 있다
1// 1. for문
2function makeArr1(a) {
3 var arr = [];
4 console.time("test1")
5 for(let i = 0; i < a; i++) {
6 arr.push(i);
7 }
8 console.timeEnd("test1")
9 return arr;
10}
11
12//2. for문 안에서 if문으로 조건 체크
13function makeArr2(a) {
14 var arr = [];
15 console.time("test2")
16 for(let i = 0; i < a; i++) {
17 if(i.constructor === Number)
18 arr.push(i);
19 }
20 console.timeEnd("test2")
21 return arr;
22}
23
24//3. for안에서 코드를 추가
25function makeArr3(a) {
26 var arr = [];
27 console.time("test3")
28 for(let i = 0; i < a; i++) {
29 if(i.constructor === Number) i = [i + 10];
30 arr.push(i);
31 }
32 console.timeEnd("test3")
33 return arr;
34}
35makeArr1(3000000); //test1: 106.338134765625ms
36makeArr2(3000000); //test2: 114.83203125ms -> if문은 성능차이 거의 없음
37makeArr3(3000000); //test3: 239.372802734375ms -> 코드추가는 좀 더 걸림

for문 안에서 코드를 추가하는게 아니면, if문으로 조건체크를 아무리 많이해도 성능차이는 거의 없다


# 자료형에 따른 기본 Iterator의 종류

  • ƒ values() {...}: 객체의 value를 리턴
  • ƒ entries() {...}: 객체의 key와 value를 리턴
    • Map의 기본 이터레이터는 entries. 구조분해하여 key/value 골라 쓸 수 있다
    • JSON데이터타입으로 Map, Set은 지원하지 않아서 자주 안쓸거다 -> 저번주 강의 참고
1Array.prototype[Symbol.iterator] // ƒ values() { ... }
2String.prototype[Symbol.iterator] // ƒ values() { ... }
3Map.prototype[Symbol.iterator] // ƒ entries() { ... }
4Set.prototype[Symbol.iterator] // ƒ values() { ... }
5NodeList.prototype[Symbol.iterator] // ƒ values() { ... }
6
7var m = new Map([["a", 1], ["b", 2]]);
8for (const a of m) console.log(a); // ["a", 1] ["b", 2]
9for (const [k, v] of m) console.log(k); // a, b
10for (const [k, v] of m) console.log(v); // 1, 2

# 함수형 프로그래밍적 사고

  • 자료형에 따라 어떤 iterator를 사용하는지 내부를 보고, 다양한 함수를 만들어 사용가능하다
  • value만 필요하면 valuesIter(), key도 필요하면 entriesIter()를 사용하면 해결된다는 사고
1function *valuesIter(obj) {
2 for (const k in obj) yield obj[k];
3}
4
5function *entriesIter(obj) {
6 for (const k in obj) yield [k, obj[k]];
7}

# collIter()의 확장

  • 자바스크립트의 모든 것은 key: value쌍 (심지어 함수도), 하지만 무엇을 key value로 볼 것인지 의미있는 기준을 가져야한다
  • 우리는 데이터로 다루기 위한 객체만을 key: value로 보자 ( 보통 function이나 Nodelist는 순회할 이유가 없다)
1const collIter = coll =>
2 coll.constructor === Object ?
3 valuesIter(coll) : coll[Symbol.iterator]();

# instanceof / constructor

  • instanceof 연산자
    • instanceof 연산자는 object의 프로토타입 체인에 constructor.prototype 이 존재하는지를 테스트 (조상님까지 다 살펴봄)
    • 즉 체이닝안에 있으면 true
  • constructor 프로퍼티
    • constructor 프로퍼티는 객체의 입장에서 자신을 생성한 객체를 나타냄 (리얼부모)
    • plain Object를 찾기위해 사용
1var func = function() {};
2var arr = [];
3var obj = {};
4
5arr instanceof Array; // true
6arr instanceof Object; // true
7func instanceof Object; // true
8obj instanceof Object; // true
9
10arr.constructor == Object; // false
11arr.constructor == Array; // true
12func.constructor == Object; // false
13func.constructor == Function; // true
14obj.constructor == Object; // true --> plain object !