2-1. 컬렉션 중심 프로그래밍 - reduce, 제너레이터, 다형성
— Functional programming — 2 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]) ); // 615console.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.338134765625ms36makeArr2(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, b10for (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; // true6arr instanceof Object; // true7func instanceof Object; // true8obj instanceof Object; // true9
10arr.constructor == Object; // false11arr.constructor == Array; // true12func.constructor == Object; // false13func.constructor == Function; // true14obj.constructor == Object; // true --> plain object !