프론트엔드/JS

[JavaScript] L.range, L.map, L.filter, take의 평가 순서 및 효율성

턴태 2023. 5. 3. 22:52
const L = {};

L.range = function *(l) {
	let i = -1;
    while (++i < l) yield i;
};

L.map = curry(function *(f, iter) {
    iter = iter[Symbol.iterator]();
    let cur;
    while (!(cur = iter.next()).done) {
	    const a = cur.value;
	    yield f(a);
    }
});

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;
    }
};

const take = curry((l, iter) => {
	const res = [];
    iter = iter[Symbol.iterator]();
    let cur;
    while (!(cur = iter.next()).done) {
	    const a = cur.value;
	    res.push(a);
        if (res.length === l) return res;
    }
    return res;
});

이전에 나온 함수들 중에 지연 평가를 하는 함수들은 위와 같다.

 

여기서 아래와 같이 위 함수들을 나열하여 다음과 같이 사용할 수 있다.

go(L.range(10),
	L.map(n => n + 10),
   	L.filter(n => n % 2),
    take(2),
    console.log);

그런데 이렇게 실행하는 경우 가장 먼저 take 함수를 실행하게 된다. 실제로 debug모드에서 멈추게 되면 take를 먼저 순회한다. 하지만 더 자세히 진행순서를 확인해보면, take 함수의 let cur; 까지 진행을 하다가 while (!(cur = iter.next()).done) { 라인을 만났을 때, filter 함수로 넘어간다.

 

L.range를 한 결과가 코드의 평가를 미뤄둔 이터레이터가 L.map으로 들어가며, 동일하게 L.map에서도 평가를 미뤄둔 L.filter로 넘어간다. 이후 최종적으로 take 함수의 인자로 들어가게 되는 것이다.

 

그래서 값을 평가하기 시작하면 take -> L.filter -> L.map -> L.range 순으로 실행된다. 이제 다시 L.range에서 값을 yield하고, L.map -> L.filter -> take 로 넘어간다.

 

즉, 각 함수마다 모두 값을 평가하여 넘어가는 것이 아니라, 0부터 10까지 각 원소가 모든 함수를 지난 다음 그 다음 원소로 넘어가게 되는 것이다 .

 


엄격한 계산과 느긋한 계산의 효율성 비교

엄격한 계산은 모든 배열을 계산한 다음 각각 절차를 진행한다.

 

느긋한 계산은 다 확인하지 않지 않고 실제 사용될 때만 절차를 진행한다.

 

그래서 예를 들어 take 함수의 인자값이 2인데, range 함수의 인자값이 100,000이라면 range에서 모든 값을 평가한다 한들 take에서 2개의 값만 뽑아낸다. 하지만, L.range와 take를 사용하면 2 번의 순회만 하고 원하는 값을 추출해낼 수 있다.

 

심지어 무한수열의 경우도 사용이 가능하다.

 


제너레이터를 활용하는 함수가 어떻게 동작하게 되는지 충분히 알 수 있었다. 평가를 뒤로 미루기 때문에 각각의 함수가 본인이 평가되는 시점에서만 값을 평가하게 되고 각 함수간의 절차가 유동적으로 변한다. 따라서 한 함수가 끝나고 나서야 다음 함수가 실행되는 비유동적인 절차를 밟지 않고 차례대로 함수를 순회한다는 점에서 좋은 방법인 것 같다. 특히 range와 take가 그러했다. range에서 모든 값을 평가하여 다음으로 넘겼을 때, 이 값들이 모두 사용되리라는 보장 없이 그저 함수를 실행시키면 효율적이지 않기 때문이다. 이는 비단 프론트엔드에만 적용할 수 있는 것이 아니라, 백엔드 소스코드에도 충분히 접목시킬 수 있을 것으로 보인다.

 

출처: 인프런 함수형 프로그래밍과 JavaScript ES6+