range 함수는 0부터 인수로 넣어준 값 만큼 순차적으로 숫자가 들어가 있는 배열을 만드는 함수다.
이번에는 제너레이터를 활용한 L.range함수를 만들었다. list2의 값은 제너레이터 객체가 나오고 밑의 reduce로 동작한 값은 동일하게 6이 나옴을 알 수 있다.
하지만 두 함수의 동작 방식에는 큰 차이가 있다.
위의 range함수는 호출 시 즉시 평가가 되어 [0,1,2,3] 이라는 배열이 생성된다.
L.range의 경우에는 제너레이터 객체
즉, 이터레이터이기 때문에 list2.next()를 하기 전까지는 어떤 코드도 동작 되지 않는다. 그렇기에 reduce 함수로 순회를 하는 시점에 값을 꺼내 사용한 것이다.
이번엔 길이와 이터레이터를 인자로 받아 전달해준 길이만큼 이터레이터를 리턴하는 take 함수를 만들었다.
이후 range와 L.range의 시간 차이를 console로 찍어보았다.
보다시피 엄청난 시간차이를 보이고 있다. range함수에 전달하는 길이 값이 커지면 커질수록 시간 차이는 엄청날 것이다. 앞에 설명한 것과 같이 range의 경우 배열을 생성하고 그 후에 take로 길이 만큼 출력한다.
반면 L.range의 경우는 연산을 미루다가 take함수로 순회할 때 필요한 값만 사용을 했기때문에 range의 인수로 Infinity가 오더라도 동작 시간의 차이가 없다.
위의 예로 보다시피 지연평가는 굉장히 영리하다.
이전에 만든 함수들도 지연성을 가진 제너레이터 함수로 변경해보자!
map함수를 제너레이터 함수로 구현했다.
마찬가지로 L.map 자체로는 새로운 array를 반환하지 않는다.
필요한 시점에 순회하며 값을 뽑아쓸 수 있도록 연산을 미룬것이다.
지연성을 가진 filter 함수도 마찬가지로 동작한다.
최종적으로 L.map과 L.filter는 curry함수로 감싸져 있다.
이제 앞서 만든 함수들을 활용해 지연성을 가진 함수의 동작 차이를 보려고 한다.
위의 go는 순차적으로 즉시 평가가 되어 예상대로 진행이 된다.
아래의 go는 L.map과 L.filter가 실행되지만 어떤 연산도 하지 않고 take함수 내부로 들어가게 된다. 이후 take함수의 iter.next() 메서드가 실행되면서 L.filter 함수로 들어가게된다. 마찬가지로 filter 내부의 iter.next()가 실행되는 순간 L.map 함수로 들어가고 L.map에서 L.range로 들어간다. 이후 L.range의 yield로 값이 평가되면 아래 순서로 흐른다.
[mapping, mapping], [filtering, filtering], [mapping, mapping]] = [[mapping, filtering, mapping], [mapping, filtering, mapping]]
유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강의