[functional JS ES6+] 지연성 1 : L.range, L.map, L.filter, L.take
작성:    
업데이트:
카테고리: Functional JS
태그: FE Language, Functional JS, JS
본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.
range 함수
일반 range 함수
숫자 하나를 받아서, 그 숫자의 크기만한 배열을 만드는 range 함수를 만들어보자
const add = (a, b) => a + b;
const range = l => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
}
log(range(5)); // [0, 1, 2, 3, 4]
log(range(2)); // [0, 1]
// range 만큼 모두 합을 누적
const list = range(5)
log(list); // [1, 2, 3, 4, 5]
log(reduce(add, list)); // 15
느긋한 range 함수: L.range
- iterator를 만드는 generator 함수를 이용
- i가 증가하면서 yield에 추가
const L = {};
L.range = function *(l) {
let i = -1;
while (++i < l) {
yield i;
}
}
// range 만큼 모두 합을 누적
const list = L.range(5)
log(list); // L.range {<suspended>}
log(reduce(add, list)); // 15
차이점 1. range에 따른 list를 출력할 때
- range 함수 : 배열 출력
- L.range 함수 : iterator 출력(next 메서드 사용 가능)
차이점 2. 실행에 대한 평가 시점
- range 함수 : 실행했을 때, 해당 부분의 표현식이 바로 값(배열)으로 평가
- L.range 함수
- 실행했을 때 순회가 모두 실행되지 않는다.
- iterable이 next가 처음 찍힐 때 함수 내부 평가
- iterable이 next가 찍힐 때마다 while문 반복 하나씩 평가
L.range = function *(l) {
log('start');
let i = -1;
while (++i < l) {
log(i, 'L.range');
yield i;
}
}
const list = L.range(5)
log(list); // L.range {<suspended>}
log(list.next());
// start
// 0 "L.range"
// {value: 0, done: false}
log(list.next());
// 1 "L.range"
// {value: 1, done: false}
log(list.next());
// 2 "L.range"
// {value: 2, done: false}
이게 왜 필요하죠?
- range()의 모든 범위를 사용하지 않을 때
- range()는 그래도 전달된 수만큼의 크기의 배열을 바로 생성
-
이 배열에서 Symbol.iterator를 통해 iterator를 생성해 next를 돌며 처리
- L.range()는 필요한 만큼만 그때그때 평가해 값을 꺼내면서 처리
- 그 자체가 iterator이므로, iterable을 만들고 iterator 생성할 필요 없이 바로 동작
- 여기에서 효율성의 차이 발생
L.range 함수의 성능 확인
그렇다면 이러한 동작이 어떤 차이를 만드는지 확인해보자
function test(name, time, f) {
console.time(name);
while (time--) f();
console.timeEnd(name);
}
test('L.range', 10, () => reduce(add, L.range(1000000))); // L.range: 296.49...ms
test('range', 10, () => reduce(add, range(1000000))); // range: 488.15...ms
나름 유의미한 효율성의 차이를 볼 수 있다.
지연평가
가장 필요할 때까지 평가를 미루다가 필요할 때 평가하여 값을 만드는 것
- 게으름과 동시에 똑똑한 방식
-
필요한 순간에 평가해 값을 만들면, 필요한 만큼만 평가를 실행하므로 불필요한 평가 회피 가능
- ES6 이전까지는 JS 공식적인 방식이 아닌, 비공식적 연산을 통해서만 구현해야 했음
- 그런데 이는 다른 라이브러리나 여러 활용 부분에서 호환이 되지 않거나 불안정한 방식
- ES6 부터 보다 더 공식적인 방식으로 지연평가를 지원
- 때문에 함수와의 합성이나 라이브러리 등의 사용에서 안정성과 효율성을 취할 수 있게 됨
- generator 기반, iterator 중심 프로그래밍에서의 지연평가 구현
take
많은 값을 받아서 잘라주는 함수
const take = (limit, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if(res.length == limit) return res;
}
}
log(take(5, range(100))); // [0, 1, 2, 3, 4]
log(take(5, L.range(100))); // [0, 1, 2, 3, 4]
- iterable protocol을 따르는 함수
- 지연성을 가지더라도 iterable protocol을 따른다면 순회해서 배열 생성 가능
- 여기에서 지연성 함수를 사용하면 최대 개수와 상관없이 5개만 생성해서 취하므로 효율성 차이가 더 극대화
L.map과 L.filter
L.map
L.map = function *(f, iter) {
for (const a of iter) yield f(a);
}
let it = L.map(a => a + 10, [1, 2, 3]);
log(it.next()); // {value: 11, done: false}
log(it.next()); // {value: 12, done: false}
log(it.next()); // {value: 13, done: false}
log([...it]) // [11, 12, 13]
- 새로운 array를 만들지 않음.
- 필요한 범위까지만 iterator의 next를 진행하며 yield를 뽑아냄.
- 필요하다면 spread를 통해 전체 배열을 순회하며 배열형태로 출력도 가능
L.filter
L.map = function *(f, iter) {
for (const a of iter) if (f(a)) yield a;
}
let it = L.filter(a => a % 2, [1, 2, 3]);
log(it.next()); // {value: 1, done: false}
log(it.next()); // {value: 3, done: false}
log(it.next()); // {value: undefined, done: true}
log([...it]) // [1, 3]
- L.map과 거의 유사
- for문으로 iteration하며 각 요소 a의 함수 반환값 f(a)가 true라면 yield a
평가 순서
즉시 평가와 게으른 평가의 평가 순서와 동작은 어떻게 다른지 확인해보자
go 함수의 내부 함수인자들은 모두 curry로 작성되어 첫 함수 인자의 값(배열)이 다음 함수의 인자로 전달
즉시 평가
go(range(10),
map(n => n + 10),
filter(n => n % 2),
take(2),
log); // [11, 13]
- range(10)이 평가되어 길이가 10인 0부터 9까지의 배열 반환
- 이 배열이 map 함수에 전달되어 각각의 값에 10을 더한 배열 반환
- 이 배열이 filter 함수에 전달되어 홀수인 값만 남긴 배열 반환
- 이 배열이 take 함수에 전달되어 값이 2개만 있는 배열 반환
- 이를 log로 출력
핵심은 두 가지이다.
- 앞에서부터 순차적으로 진행
- 함수마다 모두 평가해서 배열을 준비하고 다음 함수에 전달하며 함수 묶음 진행
게으른 평가(L. 함수 사용)
- 모양새도, 결과는 같지만 평가 순서는 정반대
go(L.range(10),
L.map(n => n + 10),
L.filter(n => n % 2),
L.take(2),
log); // [11, 13]
- 처음에 L.take 함수를 평가하여 2개의 인자만 뽑는다는 것을 확인
- 전달되는 배열 대신 iterator가 위치
- 이를 전달할 L.filter 함수 평가
- 홀수만 뽑는다는 것을 확인
- 이에 iterable을 전달할 L.map 함수 평가
- 요소들에 10씩 더한다는 사실 확인
- 이 대상이 되는 iterable을 전달하는 L.range 함수 평가
- L.range에서 yield를 1개 반환
- 이를 L.map 함수에서 실행해 10을 더하고 반환
- 이를 L.filter 함수에서 확인하여 필터 여부 판단
- 이를 통과한다면 L.take 함수에 전달
- 8~11을 반복하며 2개까지 누적
- 2개를 만족한다면 log 실행하여 출력
핵심은 두 가지이다.
- 뒤에서부터 평가를 역순으로 진행
- 하나씩 yield를 반환하지만 결과적으로 필요한만큼만 평가를 진행하여 불필요한 평가 회피 → 시공간적 효율 UP
게으른 평가의 효율성
range()의 범위가 커지게 된다면?
- 즉시 평가: 매번 큰 크기의 배열을 준비하고 내리므로 연산속도는 급격히 증가, range 인자의 크기가 지배적
- 게으른 평가: range 크기는 상관 없이 take에서 필요한 개수를 충족하면 평가를 마치므로 take 인자의 크기가 지배적
map, filter 계열 함수들의 결합 법칙
- 사용하는 데이터가 무엇이든지
- 사용하는 보조 함수가 순수 함수(ex. 사칙연산)라면 무엇이든지
- 아래와 같이 결합한다면 모두 결과가 같다.
즉시 평가
- [[mapping, mapping], [filtering, filtering], [mapping, mapping]]
- 가로형
게으른 평가
- [[mapping, filtering, mapping], [mapping, filtering, mapping]]
- 세로형
댓글남기기