숫자를 받고 순회하여 배열을 생성하는 함수를 만들었다.
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이 출력되는 것을 확인할 수 있다.
제너레이터를 이용하여 순회를 하였다.
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함수는
함수호출시 지정범위만큼 배열을 바로 생성하는 함수이다.
왜냐하면 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
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함수를 통해 지연성에 대해 더 쉽게 이해할 수 있을 것이다.
log(take(5, range(1000000)));//[0,1,2,3,4]
log(take(5, L.range(1000000)));//[0,1,2,3,4]
✅ Infinity 적용
log(take(5, range(Infinity)));//에러 발생
log(take(5, L.range(Infinity)));//[0,1,2,3,4] 무한수열을 적용해도 동작한다.
✅ currying 적용해보기
console.time('');
go(
L.range(10000),
take(5),
reduce(add),
log
);
console.timeEnd('');
기존 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.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]
✅ 지연성이 존재하지 않는 로직
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]
1️⃣ take
에서 값 호출
2️⃣ L.filter
에서 값 호출하여 상태비교 후 반환
3️⃣ L.map
에서 filter로 부터 요청받은 값을 호출하기위해 다시 range
에 요청해 받은 값에 function을 적용(n+10)
해 반환
4️⃣ L.range
는 L.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(a⇒a+10)
에서 a⇒a+10
이 보조함수이러한 지연 평가 방식이 이전의 자바스크립트에서는 굉장히 복잡하거나 지저분한 방식으로 구현할 수밖에 없었다.
왜냐하면 공식적인 자바스크립트에 있는 값이 아니라 전혀 다른 형태의 규약들을 만들어, 해당하는 라이브러리 안에서만 동작할 수 있는 방식으로 구현해야 했기 때문이다.
ES6 이후에는 자바스크립트의 공식적인 값을 통해서, 함수와 리턴되는 실제 자바스크립트의 값을 통해서 지연 여부를 확인하고 원하는 시점에 평가하겠다 등의 자바스크립트와 개발자가 약속된 규약을 가지고 만들어갈 수 있도록 개선되었다.
ES6 이후에는 지연성을 다루는 데에 있어서 자바스크립트의 고유한, 약속된 값을 통해 구현, 합성, 동작이 가능해졌다.
이러한 방식으로 구현된 지연성은 서로 다른 라이브러리 또는 서로 다른 사람들이 만든 함수 등 어디에서든지 자바스크립트의 기본 값과 기본 객체를 통해 소통하기 때문에 조합성이 높다.
출처: 함수형-프로그래밍-지연성
참조 및 참고하기 좋은 사이트