본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.
객체의 정보를 이용해 queryString을 만드는 코드를 작성해보자
const queryStr = obj => go( obj, // { limit: 10, offset: 10, type: 'notice' } object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]] map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"] reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice" ); log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
- 참고로 obj를 받아서 첫 값이 obj 그대로 사용되므로 pipe 함수를 사용해도 된다.
const queryStr = pipe( Object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]] map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"] reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice" ); log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
reduce 대신에 array에서 제공하는 join 내장함수 쓰면 안 되나요?
-
join 함수는 array 객체를 상속하는 객체에서만 사용 가능
-
말그대로 array의 prototype에만 존재하는 join 메서드이므로
-
반면 위의 reduce는 iterable이라면 모두 사용 가능
-
더 큰 다형성을 확보 가능
그렇다면 join 함수를 더 다형성이 높게 변형해보자
const join = curry((sep = ",", iter) => reduce((a, b) => `${a}${sep}${b}`, iter)); const queryStr = pipe( Object.entries, map(([k, v]) => `${k}=${v}`), join('&') ); log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
-
go나 pipe에서 인자 전달이 더 수월하게 하기 위해 curry 함수를 씌워서 작성
-
sep(결합문자)이 별도로 없는 경우 기본으로 ',' 지정
-
array 뿐만이 아니라 iterable이라면 join 함수 사용 가능
-
generator로 만든 iterable도 join 함수 사용 가능
-
join 이전의 함수들은 iterable protocol을 따르고 있어서 지연 가능
- 앞서 join 함수를 reduce 함수를 통해서 제작
- 함수형 프로그래밍은 계보를 따르도록 작성 가능
- take 함수를 통해 find 함수를 제작해보자.
find 함수: iterable에서 조건을 만족하는 첫 번째 요소를 찾는 함수
const users = [ { age: 32 }, { age: 31 }, { age: 37 }, { age: 28 }, { age: 25 }, { age: 32 }, { age: 31 }, { age: 37 }, ]; const find = curry((f, iter) => go( iter, filter(f), // [{age: 28}, {age: 25}] take(1), // [{age: 28}], 조건을 만족하는 요소 1개만 취함 ([a]) => a)); // {age: 28} : 배열을 깨는 함수 log(find(u => u.age < 30)(users)); // {age: 28}
즉시 평가 싫어요
- 위의 함수는 하나의 결과값만 꺼내더라도 이전까지 모두 순회하며 배열을 준비하는 상황
- 이를 지연 평가를 통해 불필요한 평가를 회피해보자.
const find = curry((f, iter) => go( iter, L.filter(f), 🔆 take(1), ([a]) => a)); log(find(u => u.age < 30)(users));
- L.filter 함수를 통해 take부터 평가를 시작
- take 함수에게 결과 연산을 미뤄서 하나의 값이 꺼내지면 더이상의 연산을 하지 않도록 한다.
const map = curry((f, iter) => { let res = []; iter = iter[Symbol.iterator](); let cur; while (!(cur = iter.next()).done) { const a = cur.value; res.push(f(a)); } return res; }); log(map(a => a + 10, range(4)))
위의 map 함수를 L.map을 이용해 바꿔보자.
// go 함수 기본 구조 const map = curry((f, iter) => go( iter, L.map(f), take(Infinity) )); // 초기값 iter를 첫 함수에 전달해 go 함수 축약 const map = curry((f, iter) => go( L.map(f, iter), take(Infinity) )); // go 함수를 pipe 함수로 변경 const map = curry(pipe(L.map, take(Infinity)));
// 기존 filter 함수 const filter = curry((f, iter) => { let res = []; iter = iter[Symbol.iterator](); let cur; while (!(cur = iter.next()).done) { const a = cur.value; if (f(a)) res.push(a); } return res; })
위의 filter 함수를 L.filter 함수를 통해 지연평가 방식으로 변경해보자.
// go 함수를 이용해 L.filter 처리 const filter = curry((f, iter) => go( iter, L.filter(f), take(Infinity) )); // 초기값 iter를 첫 함수에 전달해 go 함수 축약 const filter = curry((f, iter) => go( L.filter(f, iter), take(Infinity) )); // go 함수를 pipe 함수로 변경 const filter = curry(pipe(L.filter, take(Infinity)));
앞서 동작을 명확히 하기 위해 풀어쓴 L.filter 함수 코드를 리팩토링 해보자.
// 기존 L.filter L.filter = curry(function *(f, iter) { iter = iter[Symbol.iterator](); let cur; while(!(cur = iter.next()).done) { const a = cur.value; if (f(a)) { yield a; } } }); // 리팩토링 후 L.filter = curry(function *(f, iter) { for (const a of iter) { if (f(a)) yield a; } });
배열 내부에 또다른 배열들이 있는 경우 이들을 모두 구조분해해서 하나의 배열로 만드는 함수
const list = [[1, 2], 3, 4, [5, 6], [7, 8, 9]]; ??? // [1, 2, 3, 4, 5, 6, 7, 8, 9]
- 위와 같이 동작하기 위해 만들어진 함수이다.
- 이 함수를 만들어보자.
// a가 Symbol.iterator를 가지고 있다면 true // a[Symbol.iterator]가 null일 수 있으므로 'a &&'로 안전하게 처리 const isIterable = a => a && a[Symbol.iterator]; L.flatten = function *(iter) { for (const a for iter) { if (isIterable(a)) for (const b of a) yield b; else yield a; } } const it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]); log([...it]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
위의 코드를 보다 더 간단하게 바꿔보자
L.flatten = function *(iter) { for (const a for iter) { if (isIterable(a)) yield *a; else yield a; } }
yield *iter은for (const val of iter) yield val;과 같다.
L.flatten을 즉시평가하는 함수 flatten을 L.flatten 기반으로 만들어보자
const flatten = pipe(L.flatten, take(Infinity)); log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
- flatten 함수에 배열을 인자로 전달하면 다음과 같은 순서로 진행된다.
- take 함수는 limit이 Infinity이므로 L.flatten의 yield마다 take에 추가한다.
- L.flatten은 매번 next를 진행하며 yield를 반환한다.
- 이를 반복한다.
만약 배열 내부에 또 내부 배열이 있다면?
깊은 iterable을 모두 펼치는 L.deepFlat 함수를 구현해보자.
L.deepFlat = function *f(iter) { for (const a of iter) { if (isIterable(a)) yield *f(a); else yield a; } };
재귀적으로 내부 배열을 다시 f 함수로 넣어 yield를 내보내는 것이다.
flatten과 map을 동시에 실행하는 함수
- 최신 자바스크립트에 추가
- array.prototype에만 존재하는, 범용성이 떨어지는 함수
log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a)); // [1, 2, 3, 4, 5, 6, 7, 8, 9] log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a.map(a => a * a))); // [1, 4, 9, 16, 25, 36, 49, 64, 81] log(flatten([[1, 2], [3, 4], [5, 6, 7], 8, 9].map(a => a.map(a => a * a)))); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
- 자바스크립트가 기본적으로 지연적으로 동작하지 않기 때문에 추가
- array를 포함해 iterable이라면 모두 동작하도록 작성
L.flatMap = curry(pipe(L.map, L.flatten)); const flatMap = curry(pipe(L.map, flatten)); const it = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7], 8, 9]); log(it.next()); // {value: 1, done: false} log(it.next()); // {value: 4, done: false} log(it.next()); // {value: 9, done: false} log(it.next()); // {value: 16, done: false} log(it.next()); // {value: 25, done: false}
flatMap의 활용
log(flatMap(L.range, [1, 2, 3])); // [0, 0, 1, 0, 1, 2]
flatten을 이용해 2차원 배열을 다뤄보자
const arr = [ [1, 2], [3, 4, 5], [6, 7, 8], [9, 10] ]; go(arr, flatten, log); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] go(arr, flatten, filter(a => a % 2), log); // [1, 3, 5, 7, 9] go(arr, L.flatten, L.filter(a => a % 2), take(3), log); // [1, 3, 5]
데이터를 실무에서 사용하는 구조로 바꾸어 이해해보자
const users = [ { name: 'a', age: 21, family: [ {name: 'a1', age: 53}, {name: 'a2', age: 47}, {name: 'a3', age: 16}, {name: 'a4', age: 15} ] }, { name: 'b', age: 24, family: [ {name: 'b1', age: 58}, {name: 'b2', age: 51}, {name: 'b3', age: 19}, {name: 'b4', age: 22} ] }, { name: 'c', age: 31, family: [ {name: 'c1', age: 64}, {name: 'c2', age: 62} ] }, { name: 'd', age: 20, family: [ {name: 'd1', age: 42}, {name: 'd2', age: 42}, {name: 'd3', age: 11}, {name: 'd4', age: 7} ] } ];
- 위는 4명의 사람과 그 가족에 대한 데이터
go(users, // users 정보로부터 L.map(u => u.family), // 각 user의 가족들 리스트를 모아 L.flatten, // 이를 flatten하여 한 배열에 객체를 모은다. L.filter(u => u.age < 20), // 20세 미만의 미성년자만 L.map(u => u.name), // 이름만을 뽑는데, take(4), // 4명만 뽑는다. log // ["a3", "a4", "b3", "d3"] )
- 객체 지향은 데이터 중심인 반면, 함수형 프로그래밍은 이미 작성된 함수들에 데이터를 맞추는 방식
- 함수가 데이터보다 우선순위가 높다.
