자바스크립트는 지연 평가(lazy evaluation) 와 그에 따른 최적화를 지원하지 않지만, 그렇다고 지연 평가가 없지는 않습니다.
지연 평가는 말 그대로 평가를 지연하는 것이며, 연산자들과 로직을 담은 순수 함수를 통해 평가하는 일을 늦춥니다. 지연 평가를 올바르게 동작시키려면 그 평가의 단위가 반드시 순수 함수여야 합니다.
함수형 프로그래밍에서는 추상화를 함수 단위로 하고, 식과 값을 함수 단위로 다루면서 평가 시점을 정확히 다루는 식으로 로직을 설계합니다. 기본이 느긋한 계산법으로 동작하는 언어에서는 모나드를 통해 엄격한 평가를 구현하고, 기본이 엄격한 계산법으로 동작하는 언어에서는 함수를 만들어 지연 평가를 구현합니다.
결국 함수형 프로그래밍이란, 아무데서나 함수를 선언할 수 있고, 어디에서 실행 할 수 있는지, 즉시 실행이 가능한지, 언제 실행할 수 있는지, 함수를 또 다른 실행 컨텍스트로 넘겨 이어갈 수 있는지가 중요하고 이게 거의 전부입니다.
- Partial.js : L
자바스크립트는 연산을 느긋하게 하는 언어가 아니며, 평가 순서가 중요합니다.
하지만 프로그래밍에선 함수 실행 순서를 재배치하는 것만으로도 충분히 성능을 개선할 수 있습니다.
요즘 사용하는 대부분의 언어들에서는 컬렉션 관련 함수들에 대한 지연 평가를 구현할 수 있습니다. 컬렉션 관련 함수들이 연속적으로 사용되었을 때 이 함수들이 최적화될 수 있는 특정 시점으로 미루는 방식은 좋은 성능을 만들 수 있습니다.
예시를 보도록 하겠습니다.
const _ = require('partial-js');
const square = v => v * v;
_.go([2, 4, 11, 2, 7, 12],
_.map(square),
_.every(v => v < 100),
console.info); // false
배열 내부의 값을 제곱했을 때 값이 모두 100 보다 작은지 알아내려는 코드입니다.
여기선 _.map 에서 루프를 6번, _.every 에서 3번 루프를 돌겠네요.
Partial.js 에서 L 을 이용하면, 파이프라인 내부에서 함수들의 실행 순서를 재배치해 적절히 평가를 지연할 수 있습니다.
syntax 는, 단순히 지연 평가하고 싶은 함수의 네임스페이스를 _ 에서 L 로 바꿔 주면 끝입니다. (지연 평가 할 영역을 명시적으로 선택하는게 전부라고 할 수 있습니다)
const _ = require('partial-js');
const square = v => v * v;
_.go([2, 4, 11, 2, 7, 12],
_.L.map(square),
_.L.every(v => v < 100),
console.info); // false
동일한 코드를 이렇게만 바꿔주면, 지연 평가는 끝입니다.
이렇게 코드를 작성하면, 내부에선 자동 재배치를 통해 square 를 미루게 되어 L.map 에서 3번의 루프, L.every 에서 3번의 루프.
총 6번의 루프를 통해 결과를 얻을 수 있습니다. 정말 이렇게 동작하는지 console 을 좀 찍어봐야겠네요.
const _ = require('partial-js');
const square = v => {
console.info('loop');
return v * v;
};
_.go([2, 4, 11, 2, 7, 12],
_.map(square),
_.every(v => v < 100),
console.info);
// loop
// loop
// loop
// loop
// loop
// loop
// false
_.go([2, 4, 11, 2, 7, 12],
_.L.map(square),
_.L.every(v => v < 100),
console.info);
// loop
// loop
// loop
// false
원래 코드에선 square 함수가 6번 호출되었는데, L 을 사용한 코드에선 3번 호출되었네요.
루프 횟수를 줄였으니 성능을 개선했구나! 라고 박수를 쳐도 되겠습니다만, 정말 성능 개선이 되었다 라고 평할 수 있을까요?
만약 우리가 테스트하고자하는 배열이 [11, 2, 4, 2, 7, 12] 라면 1번씩만 호출되고 바로 끝입니다. 하지만 [2, 4, 2, 7, 8, 11] 이런 순서였다면, 오히려 L 을 사용하지 않은 코드가 더 낫습니다.
결론은 지연 평가는 소용이 없을 때도 있고, 있을 때도 있습니다.(?)
그렇지만 지연 평가는 언제 무엇을 해야 될지에 대한 기준이 명확히 있습니다.
const _ = require('partial-js');
_.go([ ... ],
_.map( 1 )
_.every( 2 ),
console.info);
위와 같은 상황에서 _.go 의 첫 번째 인자로 전달되는 데이터가 많을수록 지연 평가는 유리합니다.
1번의 자리에 들어가게 되는 함수가 오래 걸릴수록 뒤로 미루는게 좋으며, 2번의 자리에서 최종적으로 꺼낼 값의 갯수가 적을수록 미루는 것이 좋습니다.
만약 2번에 들어갈 함수는 빠르고, 1번에 들어갈 함수는 실행 시간이 좀 오래 걸린다고 가정해 보겠습니다.
이때 지연 평가를 하지 않는다면, 배열의 모든 값에 대해 1번 함수를 실행한 후 2번 함수를 실행하므로 완료되기까지 시간이 오래 걸립니다.
하지만 지연 평가를 한다면, 2번의 결과에 따라 1번 함수 실행을 한 번만 할 가능성도 생기기에 성능 향상이 대폭 될 수 있습니다.
물론 L 을 지원하려면 추가적인 코드 실행이 일어나지만, 이 때 발생하는 코드들은 절차지향적으로 쥐어짠 코드입니다.
따라서 이 부분에 의한 손해는 거의 없습니다.
오히려 중요한 점은 위와 같은 코드에서 첫 번째 인자로 들어올 배열의 길이는 얼마나 될지, 저 1번에 들어갈 함수가 빠른지, 느린지, 어떤 일을 하는지에 대해선 코드를 작성한 개발자만 알 수 있습니다. 즉 어떤 함수를 어떻게 지연시킬 것인가에 대한 전략은 코드를 작성하는 개발자의 역량에 달린 것입니다.
1. 데이터가 많을 때
2. 앞쪽 함수가 무거운 함수일 때
3. 뒤쪽으로 갈수록 필요한 재료가 적을 때
지연 평가의 기준은 정리하자면 위의 3가지 입니다. 코드를 작성하는 개발자가 이 기준을 가지고 어떤 순수 함수를 뒤로 미룰지 결정하면 되겠습니다.
결국 성능 개선의 대상인 컬렉션 관련 함수들을 다루는 컬렉션 중심 프로그래밍에서의 지연 평가에 대해 다음 포스팅에서 더 깊게 들어가보도록 하겠습니다.
'Backend > 함수형 자바스크립트' 카테고리의 다른 글
지연 평가 (3) (0) | 2021.10.30 |
---|---|
지연 평가 (2) (0) | 2021.10.23 |
Partial.js (3) (0) | 2021.10.09 |
Partial.js (2) (0) | 2021.10.02 |
Partial.js (1) (0) | 2021.09.25 |