게으른 평가
- call-by-need 라고도 불리는 이 전략 방법은 ( 반대는 call-by-name ) 값이 실제로 필요할때 평가를 하는것이다. 또한 반복적인 평가를 피하기 위해서 한번 평가해둔 자료는 기억해 둔다.(memoization)
왜?
- 아래와 같은 코드를 생각해 보자.
const someValue = expensiveFunction();
//.. someValue를 사용하지 않는 다른 연산 코드들 및 유저 인터페이스를 포함하고 있는 수많은 코드들..
console.log(someValue)
- someValue는 코드의 맨 마지막 console.log 찍을때 필요하다.
- 처음에 expensiveFunction을 호출할때 브라우저는 그 시간동안 아무것도 안한다. 그 이후에 있을 사용자 경험을 높이기 위한 코드를 지연시키고 따라서 사용자 경험을 방해했을 것이다. 이것은 문제다.
- 물론 아래와 같이 바꿀수 있다.
//.. someValue를 사용하지 않는 다른 연산 코드들 및 유저 인터페이스를 포함하고 있는 수많은 코드들..
console.log(expensiveFunction());
- 아름답지 않을 뿐더러 큰 프로젝트일수록 언제 쓰일지도 모르고 필요할때마다 매번 호출하게 되니 비용이 많이 든다.
이렇게 하고 싶다.
- 만약 평가를 정말 필요할때 하면 어떨까?
- 자바스크립트에서 lazy 라는 키워드가 있다면..
- someValue에는 실제 평가를 진행하는 코드가 있을테고 해당 값을 호출하면 그때 실제로 작동하는 코드이다.
- 아래와 같이 쓸수 있을 것이다.
lazy const someValue = expensiveFunction();
//... 수많은 코드들..
console.log(soameValue);
console.log(soameValue);
- 다만, 여기서 2번 반복해서 썼다고 해서 평가를 2번하는건 비효율 적이다.
만들어 보자.
- 실제 자바스크립트 안에는 lazy 라는 키워드가 없기 때문에 lazy 라는 함수를 만들어 보자.
memoization 패턴을 이용한 반복호출을 피하자.
const lazy = getter => {
let evaluated = false;
let _res = null;
const res = function(){
if(evaluated) return _res;
const _res = getter.apply(this, arguments);
evaluated = true;
return _res;
}
return res;
}
- lazy 함수는 getter 라는 함수를 인자로 받아서 getter를 호출시켜주는 새로운 함수를 반환한다.
- 클로저를 사용, evaluated 와 _res 변수는 반복 호출을 피하기 위한 자유변수들이다.
사용해보자.
let counter = 0;
const lazyVal = lazy(() => {
counter += 1;
return 'result';
})
console.log(counter); // 0
console.log(lazyVal()); // result
console.log(counter); // 1
console.log(lazyVal()); // result
console.log(counter); // 1
console.log(lazyVal()); // result
- 여러번 호출하더라도 한번만 호출이 된다는걸 알수 있다.
- 이제 실질적으로 사용하면 되는가????
- 실제 lazyValue들 끼리 연산작업이 필요할 경우엔 어떻게 해야할까???
const actualVal1 = lazyVal1();
const actualVal2 = lazyVal2();
console.log(actualVal1 + actualVal1);
- 음..달라진게 없는거 같다.
- 우리는 평가를 실제로 필요한 지점에서 사용하고 싶은데 그럼 어떻게 해야할까.
const newVal = lazy(() => {
const actualVal1 = lazyVal1();
const actualVal2 = lazyVal2();
return actualVal1 + actualVal2;
})
- 다시 lazy로 감싸야한다.
- 음.. 아름답지가 않다.
업그레이드 하자
- 우리가 원하는 시점으로 평가를 할수있도록 뒤로 늦추긴 했지만 매번 lazy로 감싸야 하는 번거로움이 있었다.
- 체이닝으로 엮어서 표현하면 어떨까???
- 다시 lazy 함수를 업그레이드 해보자.
이런 모양이면 좋겠다.
let counter = 0;
const lazyVal = lazy(() => {
return counter += 1;
})
/* 첫 평가 실행후 리턴 된 값으로 다시 lazy를 리턴 */
const lazyOp = lazyVal.then(v1 => lazy(()=> {
return v1 + 1;
}))
lazy를 수정하자.
const lazy = getter => {
let evaluated = false;
let _res = null;
const res = function(){
if(evaluated) return _res;
const _res = getter.apply(this, arguments);
evaluated = true;
return _res;
}
/* 체이닝을 위한 then 함수 생성 */
res.then = modifier => modifier(res());
return res;
}
- 리턴된 inner함수 (res) 에게 프로퍼티로 then 함수를 추가.
- then에서 인자로는 첫번째 평가 이후에 리턴된 값으로 다시 lazy를 수행할수 있는 next 함수를 받는다.
- 여기서 modifier 인자는 v1 => lazy() 함수가 되겠다.
- res()는 lazyVal 함수가 될것이고 여기서 res() 호출된 결과는 즉, v1으로 들어갈 것이다.
- 받은 v1을 다시 lazy로 감싸서 lazyOp로 return 한다.
map 함수를 만들어 보자.
- then 함수는 임의로 우리가 lazy 함수를 리턴해줬었어야 했다.
- 이번엔 그것마저 자동으로 해주는 map 함수를 만들어보자.
이런 모양이어야 한다.
let counter = 0;
/* 첫 평가 실행후 리턴 된 값으로 다시 lazy를 리턴 */
const lazyOp = lazy(() => counter += 1)
.map(v1 =>
v1 + 1
)
.map(v2 =>
v2 + 1
)
다시 lazy 함수를 수정하자.
const lazy = getter => {
let evaluated = false;
let _res = null;
const res = function(){
if(evaluated) return _res;
const _res = getter.apply(this, arguments);
evaluated = true;
return _res;
}
/* 체이닝을 위한 then 함수 생성 */
res.then = modifier => modifier(res());
/* map 함수 */
res.map = mapper => lazy(() => mapper(res()));
return res;
}
정리
- 자바스크립트에서의 게으른 평가는 결국 호출하고 싶은 코드들을 함수로 한번 더 감싸 실제 필요할때 평가한다.
- lazy 체이닝의 경우 연속된 함수 참조에 의해서 이뤄진다.
참고
https://www.codementor.io/agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3