JavaScript - 지연성(1)

박정호·2022년 10월 26일
0

JS

목록 보기
20/24
post-thumbnail

⭐️ range & L.range

range

숫자를 받고 순회하여 배열을 생성하는 함수를 만들었다.

 const range = l => {
    let i = -1;
    let res = [];
    while (++i < l) {
      res.push(i);
    }
    return res;
  };

  var list = range(4);
  
  log(list); // [0,1,2,3]
  log(reduce(add, list)); // 6

✅ list를 출력하면 4크기만큼의 배열이 출력되고, reduce를 통해서는 총합 6이 출력되는 것을 확인할 수 있다.

L.range

제너레이터를 이용하여 순회를 하였다.

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

log(list); // L.range {<suspended>}
log(reduce(add, list)); // 6

✅ list를 출력하면 배열 출력이 아닌, Iterator가 출력되는 것을 확인할 수 있다.

range vs L.range

range함수는 함수호출시 지정범위만큼 배열을 바로 생성하는 함수이다.

왜냐하면 range함수는 함수 호출시 이미 배열로 평가가되어 list에 대입되기 때문이다.

L.range함수는 배열을 미리 생성하지 않고, 평가할때 동적으로 생성하는 함수이다.

왜냐하면 L.range함수는 함수 호출시 내부의 어떤 로직도 동작하지 않다가 list.next()를 통해 순회할 때 결과가 출력되기 때문이다.
(L.range는 배열을 만들지 않고 yield를 통해 값을 하나 하나 만들면서 reduce를 실행)

👉 다시 말해, range는 reduce전에 배열로 평가되어지고, L.range는 reduce에서 값을 필요로 하기 전까지는 배열로 평가되지 않는다.

이를 통해 둘이 지연평가 비교를 확인할 수 있다.

💡 중요) 지연 평가

range를 호출시 즉시 배열이 생성되지만, 해당 배열을 사용하는 실제 로직이 실행되기전까진 해당 배열의 필요성 및 중요도는 높지 않다.

반면 지연평가는 배열을 생성하는 함수를 호출한시점에서 실제 배열을 반환하지는 않는다. 실제로 Iterator가 순회를하며 next값을 꺼낼때 값을 생성하여 반환한다.

이처럼 값을 실제 사용할 때까지 계산을 늦춰서 얻을 수 있는 이점은 불필요한 계산을 하지 않으므로 빠른 계산이 가능하고 무한자료 구조를 사용할수도 있는 것이다.

range와 L.range 테스트

  • L.range이 효율성이 더 좋은 것을 확인할 수 있다.
function test(name, time, f) {
    console.time(name);
    while (time --)f();
    console.timeEnd(name);
}

test('range', 10, () => reduce(add, range(1000000))); // 379.27294921875ms
test('L.range', 10, () => reduce(add, L.range(1000000)));// 268.756103515625ms

⭐️ take

Iterable에서 원하는 length만큼의 값만 가져오는 take라는 함수를 구현해보자.

const take = (l, iter) => {
    let res = [];
    for (const a of iter) {
        res.push(a);
        if (res.length === l) return res;

    }
    return res;
};

take함수를 통해 지연성에 대해 더 쉽게 이해할 수 있을 것이다.

  • 아래의 코드는 유사하게 보이지만, range는 실제로 1000000개의 배열을 생성하고 5개의 배열을 출력한 것이며, L.range는 원하는 length만큼의 값만 출력한 것이다.
log(take(5, range(1000000)));//[0,1,2,3,4]
log(take(5, L.range(1000000)));//[0,1,2,3,4]

Infinity 적용

  • 따라서, range는 무한대로 배열을 생성하기 때문에 Error가 발생하지만, L.range는 length만큼의 필요한 범위의 값만 반환하기 때문에 정상적으로 작동한다.
log(take(5, range(Infinity)));//에러 발생
log(take(5, L.range(Infinity)));//[0,1,2,3,4] 무한수열을 적용해도 동작한다. 

currying 적용해보기

  • 앞서 배운 curry함수 방식으로 함수를 호출해보자.
    다음과 같은 최종적으로 reduce에 대한 값을 반환할 때까지 최대한 연산을 미루는 것(지연성)을 알 수 있다.
console.time('');
go(
    L.range(10000),
    take(5),
    reduce(add),
    log
);
console.timeEnd('');

⭐️ L.map

기존 map에서 제너레이터/이터레이터기반의 지연성을 가진 L.map을 구현해보자.

✅ 다음 코드는 L.map 함수에 구현하고자하는 함수로직과 배열을 넘겨 원하는 결과를 얻으려한다.
하지만, 아래의 경우처럼 함수를 호출할 당시에는 평가가 되지 않아서 아무런 결과를 반환하지 않는다.

L.map = function* (f, iter) {
    for (const a of iter) {
        yield f(a);
    }
};
var it = L.map(a=>1+10, [1, 2, 3]);

✅ 아래처럼 next()를 통해서 호출하거나 나머지연산자를 이용해야 결과값을 반환받을 수 있다.
(L.map 역시 지연성평가에 대한 부분을 고려한 로직이라고 할 수 있다.)

L.map = function* (f, iter) {
    for (const a of iter) {
        yield f(a);
    }
};
var it = L.map(a=>1+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.next()); //{value: undefined, done: true}

var it = L.map(a=>1+10, [1, 2, 3]);
log([...it]) //[11,12,13]

⭐️ L.filter

✅ L.map과 같이 제너레이터 함수를 통해서 L.filter를 구현해보자.
단, value가 undefined이거나 done이 true가 될 때까지 값을 생성하여 반환한 것을 볼 수 있다.

L.filter = function* (f, iter) {
    for (const a of iter) {
        if(f(a)) yield a;
    }
};
let it = L.filter(a => a % 2, [1, 2, 3, 4]);
log(it.next()); //{value: 1, done: false}
log(it.next()); //{value: 1, done: false}
log(it.next()); //{value: undefined, done: true}
log(it.next()); //{value: undefined, done: true}

it = L.filter(a => a % 2, [1, 2, 3, 4]);
log([...it]); // [1,3]

⭐️ 지연성 여부에 따른 중첩 함수 비교

👉 range, map, filter, take, reduce 중첩 사용

지연성이 존재하지 않는 로직

1️⃣ range에서 값 생성 ⇒ [0,1,2,3,4,5,6,7,8,9]
2️⃣ map에서 값 조작 ⇒ [10,11,12,13,14,15,16,17,18,19]
3️⃣ filter에서 값 필터링 ⇒ [11,13,15,17,19]
4️⃣ take에서 limit ⇒ [11,13]
5️⃣ log에서 출력

go(range(10),
    map(n => n + 10),
    filter(n => n % 2),
    take(2),
    log
); // [11,13]

👉 L.range, L.map, L.filter, take 중첩사용

1️⃣ take에서 값 호출

2️⃣ L.filter에서 값 호출하여 상태비교 후 반환

3️⃣ L.map에서 filter로 부터 요청받은 값을 호출하기위해 다시 range에 요청해 받은 값에 function을 적용(n+10)해 반환

4️⃣ L.rangeL.map으로부터 요청을받으면 실제 값을 Generated해서 반환

  • 실제 값들이 Generated 되는 순서는 기존 즉시평가와는 다르게 역순인 take→L.filter → L.map → L.range순서다.

  • take에서 값을 꺼내려하면 L.filter에게 요청하게되고 L.filter는 map에게 요청해서 받은 값이 condition과 비교 후 true일때만 반환

Generator protocol 방식은 값의 흐름이 가로가 아닌 세로가 된다.

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) => {
    let 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),
    log
); // [11,13]

출처: https://catsbi.oopy.io/2570ffc6-cdf2-4f4c-8c0b-98922d9c58ba

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

지연성이 없는 중첩 함수의 경우 함수 첫 부분부터 평가를 끝내면서 모든 값이 생성되어진 상태의 요소들에 조건들을 검사하며 호출이 종료된다.

반면에 지연성이 있는 중첩 함수의 경우 평가가 필요한 호출에 의해서만 평가가 이루어지며 값 1개를 생성되어진 상태를 가지고 요소에 조건들을 검사하며 호출이 종료되어 진다.

예를 들어 range에 주어진 값이 엄청 나게 커지게 된다면 지연성이 없는 경우는 엄청 큰 범위의 모든 값을 생성하고 또 모든 값의 조건을 비교하며 실행이 되어지지만 지연성이 있는 경우에는 좀 더 빠른 평가가 이루어진다.

출처: 함수형-프로그래밍-지연성

map, filter계열 함수들이 가지는 결합 법칙

map, filter 계열 함수에는 특정한 방식으로 다르게 평가 순서를 바꾸어도 똑같은 결과를 만든다는 결합 법칙이 있다.

사용하는 데이터가 무엇이든지 사용하는 보조함수가 순수 함수라면 결과가 동일하게 반환된다.

  • map(a⇒a+10) 에서 a⇒a+10이 보조함수

ES6의 기본 규약을 통한 지연 평가의 장점

이러한 지연 평가 방식이 이전의 자바스크립트에서는 굉장히 복잡하거나 지저분한 방식으로 구현할 수밖에 없었다.

왜냐하면 공식적인 자바스크립트에 있는 값이 아니라 전혀 다른 형태의 규약들을 만들어, 해당하는 라이브러리 안에서만 동작할 수 있는 방식으로 구현해야 했기 때문이다.

ES6 이후에는 자바스크립트의 공식적인 값을 통해서, 함수와 리턴되는 실제 자바스크립트의 값을 통해서 지연 여부를 확인하고 원하는 시점에 평가하겠다 등의 자바스크립트와 개발자가 약속된 규약을 가지고 만들어갈 수 있도록 개선되었다.

ES6 이후에는 지연성을 다루는 데에 있어서 자바스크립트의 고유한, 약속된 값을 통해 구현, 합성, 동작이 가능해졌다.

이러한 방식으로 구현된 지연성은 서로 다른 라이브러리 또는 서로 다른 사람들이 만든 함수 등 어디에서든지 자바스크립트의 기본 값과 기본 객체를 통해 소통하기 때문에 조합성이 높다.

출처: 함수형-프로그래밍-지연성

참조 및 참고하기 좋은 사이트

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글