-
[Functional] 지연 평가와 L.map, L.filterprograming/Language 2019. 7. 22. 17:40
안녕하세요, Einere입니다.
(ADblock을 꺼주시면 감사하겠습니다.)
2019/06/25 - [programing/JavaScript] - [Functional] 평가와 일급, 고차함수
2019/07/15 - [programing/JavaScript] - [Functional] 순회와 이터러블
2019/07/15 - [programing/JavaScript] - [Functional] 제네레이터와 이터러블
2019/07/15 - [programing/JavaScript] - [Functional] map, filter, reduce
2019/07/18 - [programing/JavaScript] - [Functional] 코드를 값으로 다루어 표현력 높이기
2019/07/22 - [programing/JavaScript] - [Functional] range와 느긋한 L.range, take
지연 평가(lazy evaluation)
- 제때 계산
- 느긋한 계산
- 제네레이터와 이터레이터를 이용한 구현
L.map
const L = {}; L.map = function *(func, iterable) { for(const e of iterable) yield func(e); }; const result = L.map(v=>v*v, [1, 2, 3]); console.log(result.next().value); // 1 출력 console.log(result.next().value); // 4 출력 console.log(result.next().value); // 9 출력
이터러블을 순회하면서, 각 요소에 대해 함수를 적용한 값을
yield
를 통해 게으른 평가를 수행합니다.비동기 상황(인자가 promise인 경우)를 대비해, 다음과 같이 수정할 수 있습니다.
L.branch = (val, f) => val instanceof Promise ? val.then(f) : f(val); L.map = L.curry(function* (f, iterable) { for (const e of iterable) yield L.branch(e, f); });
L.take
함수에서 promise의 결과 값을 추출하므로,L.branch
를 이용하여 프로미스를 반환해도 괜찮습니다. (e
가 promise 인스턴스인 경우,val.then
의 반환값은 pending상태의 promise가 됩니다. 그리고 해당 promise의 결과 값은 then의 인자로 준f
의 반환값이 됩니다.)L.filter
const L = {}; L.filter = function *(func, iterable) { for(const e of iterable) if(func(e)) yield e; }; const result = L.filter(v => v%2, [1, 2, 3, 4, 5, 6]); console.log(result.next().value); // 1 출력 console.log(result.next().value); // 3 출력 console.log(result.next().value); // 5 출력
map과 유사합니다.
다만, 콜백함수의 결과값이 참인 경우에만 값을 발생시킵니다.
비동기 상황(인자가 pomise인 경우)를 대비해, 다음과 같이 수정할 수 있습니다.
L.nop = Symbol("nop"); L.filter = L.curry(function* (f, iterable) { // for (const e of iterable) if (f(e)) yield e; for (const e of iterable) { const result = L.branch(e, f); if (result instanceof Promise) yield result.then((val) => val ? e : Promise.reject(L.nop)); else if (result) yield e; } });
마찬가지로,
L.take
함수에서 promise의 결과값을 추출하므로,result.then
의 결과를yield
하면 됩니다.느긋한 계산의 장점
- 필요한 정보만 평가하고 사용하므로, 인풋 데이터가 클 수록 빠르다.
예를 들어, 다음과 같은 코드가 있다고 합시다.
go(range(10), map(e => e + 10), filter(e => e % 2), take(3), console.log ); // 평가 순서 // range : [0, 1, ..., 9] -> // map : [10, 11, ..., 19] -> // filter : [11, 13, ..., 19] -> // take : [11, 13, 15] -> // console.log go(L.range(10), L.map(e => e + 10), L.filter(e => e % 2), take(3), console.log ); // 평가 순서 // range : 0 -> map : 10 -> filter : false // range : 1 -> map : 11 -> filter : true -> take : [11] // range : 2 -> map : 12 -> filter : false // range : 3 -> map : 13 -> filter : true -> take : [11, 13] // ... // console.log
즉시 계산을 한다면, range에서 0부터 9를 담은 배열을 먼저 생성한 후, 해당 배열을 map으로 넘기고, 그 결과 배열을 다시 filter로 넘기는 식으로 진행됩니다.
만약 range의 인자로 10000이 들어왔다면 길이가 10000인 배열이 생성되고 다시 인자로 전달되겠죠. 당장 취할 요소는 3개 뿐인데 잉여로운 9997개의 나머지 요소를 미리 만들어두는 셈입니다.
이렇게 인자의 크기가 커질수록 즉시 계산은 비효율적이게 됩니다.
그러나 느긋한 계산은 0을 생성하고, map이 이 요소를 받아 10을 만들고, filter가 해당 값을 평가합니다.
이렇게 느근한 계싼은 당장 취하고자 하는 요소별로, 동적으로 평가하여 그 때 마다 취하기 때문에, 인자의 크기가 커져도 계산이 비효율적이지 않습니다.
map, filter의 특징
- 인풋 데이터가 무엇이든 상관없이, 보조함수(인자로 받는 함수)가 순수함수라면, 결합법칙이 성립한다.
즉, 특정 두 요소에 대해 [map, map] -> [filter, filter] -> [map, map]의 결과는 [map -> filter -> map, map -> filter -> map]의 결과와 동일하다는 뜻입니다.
예를 들어, [0, 1, ..., 9]에 map을 우선 다 적용한 뒤 filter를 적용한 결과나, 0부터 map과 filter를 차례로 적용하여 9까지 반복한 결과나 동일하다는 뜻입니다.
참고
마이클 포거스, 『함수형 자바스크립트 : 새롭고 올바른 자바스크립트 프로그래밍 기법』, 한빛미디어(2014).
유인동, 인프런 - 함수형 프로그래밍과 JavaScript ES6+
'programing > Language' 카테고리의 다른 글
[JS] state와 mutable, immutable (0) 2019.07.23 [JS] promise와 async await의 차이 (0) 2019.07.23 [Functional] range, take, 느긋한 L.range, L.take, L.reduce (0) 2019.07.22 [JS] clean code (0) 2019.07.20 [Functional] 코드를 값으로 다루어 표현력 높이기 (0) 2019.07.18 댓글