최근 <자바스크립트는 왜 그 모양일까?>를 읽고 있다. 이 책은 자바스크립트 개발에 참여하고, JSON 포맷을 창시한 더글라스 크락포드가 지었으며, 저자가 생각하는 자바스크립트의 설계 결함을 소개하고, 좀 더 아름답게 자바스크립트를 사용하는 방법을 소개하는 책이다. 그 중, 클로저를 이용하여 자바스크립트의 제네레이터를 개선하는 부분을 소개하고자 한다.
function* sequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
for(let value=start; value<end; value+=step)
{
yield value;
}
}
const iterator = sequence(1, 4, 1);
iterator.next(); // {value:1, done: false}
iterator.next(); // {value:2, done: false}
iterator.next(); // {value:3, done: false}
iterator.next(); // {value:undefined, done: true}
for(let value of sequence(0, 6, 2))
{
console.log(value); // 0, 2, 4가 순차적으로 출력됨
}
const arr = [...sequence(1,10,1)]; // [1,2,3,4,5,6,7,8,9]
쉽게 말하면, 순열을 생성하는 함수라고 할 수 있다. 그런데, 그 순열의 형태가 배열인 것이 아니라, 한 번 호출할 때마다 순차적으로 다른 값을 반환하는 객체 같은 형태(제네레이터 객체)로 생성된다.
파이썬이나 자바스크립트에서는 yield라는 문법을 이용하여 구현된다. 이 때, 제네레이터 객체가 한 번 소비되면(next 함수를 호출하거나, 반복문에서 한 번의 반복을 할 때를 의미한다) 제네레이터 함수에 있는 yield가 있는 문까지 실행되고 함수를 종료한다. 그리고 제네레이터 객체가 다음으로 소비될 때는 이전에 멈췄던 yield 다음 문부터 시작한다. 그렇기 때문에, 제네레이터 함수를 아무 때나 멈출 수 있고 상태를 유지하는 함수로 부르기도 한다.
자바스크립트에는 ES6부터 도입되었으며, 자바스크립트는 제네레이터 객체를 반복 가능 프로토콜과 반복자 프로토콜을 모두 준수하는 객체로 구현한다. 반복 가능 프로토콜과 반복자 프로토콜을 통틀어 이터레이션 프로토콜이라고 한다.
Symbol.iterator
메소드가 구현되어 있다. 이 메소드의 반환값은 반복자 프로토콜을 준수하는 객체여야 한다. 이 프로토콜을 만족하면 객체를 for of
문으로 순회할 수 있으며, 배열 구조 분해 할당으로 배열화할 수 있다.next
메소드가 구현되어 있다. 이 메소드의 반환값은 {value, done}
형태의 객체여야 하다.자바스크립트의 제네레이터는 엉성한 객체 지향 프로그래밍의 결과물입니다.
자바스크립트 제네레이터 객체를 사용하는 문법이 비직관적이기 때문이다. 저자는 자바스크립트의 제네레이터 문법을 만들 때 엉성한 객체 지향 프로그래밍의 패러다임을 버리지 못했다고 지적한다.
const iterator = sequence(1, 4, 1);
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
보는 바와 같이, 자바스크립트에서 수동으로 제네레이터 객체를 소비해 값을 얻기 위해서는 (이터레이터).next().value로 객체의 내용을 2번 추적해야 한다. 이 얼마나 번거롭고 비직관적인가! 자바스크립트의 제네레이터 문법을 모르는 사람들은 next가 무엇을 하는 것이며 왜 그 값의 value를 참조해야 하는지 모를 것이다.
function* sequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
for(let value=start; value<end; value+=step)
{
yield value;
}
}
저자는 제네레이터 함수를 만드는 yield 문의 구조적 문제에 대해서도 지적한다. yield문은 근본적으로 명령을 중단하고 다시 시작한다는 의미를 갖고 있기 때문에, 제네레이터 객체가 한 번 소비될 때에만 집중하는 것이 아니라, 제네레이터 객체의 전체의 절차에 집중하게 되는 명령형 프로그래밍에 더 가깝게 프로그래밍하게 된다. 당연히, 여러 번 소비되는 제네레이터에 대해서는 for문과 같은 반복문을 사용할 수밖에 없고, 이것이 아름다운 프로그래밍을 하지 못한다고 저자는 지적한다. (저자는 for문과 같은 반복문을 좋게 보지 않는다.)
또한, 함수를 중단하고 재시도한다는 yield문의 특성상 실행 흐름의 추적이 어려워져서 예측 불가능한 프로그램이 생성될 수 있다는 문제도 있다고 한다.
function closerSequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
let state = start; // initial value
return function()
{
let value = state;
state += step;
if(state > end) return; // generator end condition
return value; // generator yield value
}
}
const iterator = closerSequence(1, 4, 1);
console.log(iterator()); // 1
console.log(iterator()); // 2
저자는 대안으로 클로저를 이용한 제네레이터 기능을 제안한다. 제네레이터를 클로저로 구현한다면, 자바스크립트의 제네레이터 문법에서 다음의 두 문제가 해결된다.
물론, 클로저를 이용한 제네레이터의 구현은 자바스크립트의 클로저와 재귀함수에 대한 이해가 있어야 하며, 절차적으로 사고하는 초보 개발자들이 이해하기 어렵다는 문제는 있다. 또한, 자바스크립트의 이터레이션 프로토콜을 전혀 준수하지 않기 때문에, 바닐라 자바스크립트의 for of 문이나 배열 구조 분해 할당 등 유용한 기능을 사용하려면 별개의 변환 과정을 거쳐야 한다. 변환 함수는 다음에 소개하겠다.
function generatorToCloser(generator)
{
return function()
{
return generator.next().value;
}
}
const arr = [2025, 1, 3];
const iter = generatorToCloser(arr[Symbol.iterator]());
iter(); // 2025
iter(); // 1
iter(); // 3
iter(); // undefined
간단하게 바닐라 제네레이터 / 클로저 제네레이터 변환 함수를 만들어 봤다.
우선, 바닐라 제네레이터 객체를 클로저 제네레이터 함수로 변환하는 함수다. 인자로 바닐라 제네레이터 객체를 받으며, 각각의 제네레이터 소비 시에는 generator.next().value
를 호출해 소비하는 것이 전부다.
function* closerToGenerator(generator)
{
let yielder = generator();
while(yielder !== undefined)
{
yield yielder;
yielder = generator();
}
}
function closerSequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
let state = start; // initial value
return function()
{
let value = state;
state += step;
if(state > end) return; // generator end condition
return value; // generator yield value
}
}
const iter = closerToGenerator(closerSequence(0, 5, 1));
[...iter] // [0, 1, 2, 3, 4]
다음으로, 클로저를 바닐라 제네레이터 객체로 전환하는 제네레이터 함수다. 클로저 제네레이터를 인자로 받아, 바닐라 제네레이터 객체를 반환한다.
클로저 제네레이터를 값이 undefined가 나올 때까지 반복 호출하나, 값이 나왔을 때 yield문으로 제네레이터 외부로 값을 내보낸다. 이를 이용하면 클로저로 우아하게 제네레이터 코드를 작성하면서도 바닐라 자바스크립트의 이터러블 관련 기능도 누릴 수 있게 된다.
이 파트에서는 <자바스크립트는 왜 그 모양일까?>에 소개된 클로저 제네레이터 팩토리 함수를 각각 소개하고(본인의 스타일로 다시 구현했기에 원본 서적의 코드와 다를 수 있다.), 바닐라 자바스크립트의 제네레이터 문법으로는 어떻게 구현되는지 알아보도록 하겠다.
function integer(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
let state = start;
return function()
{
let value = state;
state += step;
if(step > 0 && state > end) return;
if(step < 0 && state < end) return;
return value;
}
}
Python의 range와 비슷하게, 등차순열을 반환하는 제네레이터를 생성한다. from에서 시작하고, step만큼 이동하며, 만약 값이 end 값을 넘어간다면 제네레이터를 종료한다.
해설하자면, integer 팩토리 함수가 호출될 때, state 변수(와 integer 함수의 매개변수들)가 저장되어 있는 객체를 생성한다. 이후, 제네레이터 함수가 반환되는데, 제네레이터 함수는 integer 팩토리 함수가 생성한 state 변수가 있는 객체를 참조하고, state 변수의 값을 바꿀 수 있다.
이후 제네레이터 함수가 한 번 호출될 때마다, 다음을 실행한다.
이러한 동작을 통해, 제네레이터 함수가 호출될 때마다 함수가 참조하는 state 상태(함수가 생성될 떄 같이 생성되었다)를 변경시키고, 함수가 호출될 때마다 상태를 변경시키면서 순열을 반환하게 된다.
똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.
function* integer(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
let state = start;
while(true)
{
let value = state;
state += step;
if(step > 0 && state > end) return;
if(step < 0 && state < end) return;
yield value;
}
}
이 동작은 위의 클로저 제네레이터와 최대한 비슷하게 변환한 것이다. return function()
부분을 white(true)
로, return value
를 yield value
로 변환하면 다를 바 없다. 무한루프가 일어나는 것으로 보이지만 실행 시 무한루프는 일어나지 않는데, 제네레이터 객체가 한 번 소비될 때마다 yield value
또는 return
문에서 멈추기 때문이다.
function element(arr, generator = integer())
{
return function(...args)
{
let index = generator(...args);
if(index === undefined) return;
return arr[index];
}
}
배열을 순회하는 제네레이터를 생성한다. 기본적으로 배열을 인자로 받으며, 배열의 원소를 순차적으로 순회하는 순열을 생성한다.
이 제네레이터는 2번째 인자로 다른 제네레이터를 받을 수 있는데, 다른 제네레이터의 결과를 배열의 해당하는 원소로 매핑시키는 역할을 수행한다. 일종의 함자와 같은 역할이라고 할 수 있겠다. 만약 제네레이터에 배열처럼 map 메소드가 존재한다면, 다음과 같은 느낌일 것이다.
generator.map( item=>arr[item] )
함자란? : 간단히 말하면, 집합에 함수를 통째로 적용시켜서 다른 집합으로 바꾸는 것이다. 이 때 집합의 각 원소 간 상관관계는 보존된다. 더 간단히 말하면 그냥
map
연산이라고 생각하면 된다.이 설명은 매우 단순화된 설명으로, 자세히 말하면 다음과 같다.
우리가 말하는 오브젝트가 있다. 오브젝트는 자연수나, 정수나, 문자열이나, 벡터나, 혹은 다른 집합이 될 수도 있다. 오브젝트와 오브젝트를 함수처럼 연관시킬 수도 있는데, 이를 범주론에서는 사상이라고 한다. 범주는 오브젝트와 사상을 모은 것이라고 할 수 있다.
여기서 우리는 범주를 다른 범주로 바꾸는 관계를 생각할 수 있을 것이다. 그걸 함자라고 하자. 함자는 대상을 다른 대상으로 바꾸는 것은 사상과 같으나, 사상 역시 다른 사상으로 바꿀 수 있다. 여기서 사상을 다른 사상으로 바꾼다는 것은 원소에 함자를 먼저 적용시킨 뒤 적용시키는 함수와, 원소에 함수를 먼저 적용한 뒤 함자를 적용하는 것이 같음을 의미한다.
함자는 항등사상(자기 자신을 반환하는 함수와도 같다)을 보존하며, 사상 합성(함수의 합성과도 같다) 역시 보존한다.자바스크립트의 map 연산을 함자와 매우 유사한 역할을 하는 것로 볼 수 있는데, map 연산은 배열의 원소를 다른 원소로 치환하며, 배열의 원소 간 순서 관계를 사상이라고 생각한다면, 원소 간 순서는 변하지 않으므로 구조를 보존하면서 사상을 치환한 것이라고 할 수 있다.
똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.
function* element(arr, generator = integer())
{
for( let index of generator )
{
yield arr[index];
}
}
이 함수를 활용하여, 객체의 key를 순회하는 제네레이터 팩토리 함수를 추가로 만들 수도 있다.
function keys(obj)
{
return element(Object.keys(obj));
}
function property(obj, generator = element(Object.keys(obj)))
{
return function(...args)
{
let key = generator(...args);
if(key === undefined) return;
return [key, obj[key]];
}
}
Object.entries와 비슷하게, key값과 value값의 튜플을 반환하는 제네레이터를 생성한다. 기본적으로 객체를 인자로 받으며, 객체의 key값을 순회하여 key와 value의 엔트리를 만든다. property 함수는 입력한 제네레이터 함수를 [item, obj[item]]
과 같은 관계로 매핑한다.
똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.
function* property(obj, generator = element(Object.keys(obj)))
{
for( let index of generator )
{
yield [index, arr[index]];
}
}
function collect(generator, arr = [])
{
return function(...args)
{
let value = generator(...args);
if(value === undefined) return;
arr.push(value);
return value;
}
}
이 제네레이터 함수는 제네레이터를 그냥 실행하는 것과 동일하나, 사이드 이펙트를 발동시킨다. 제네레이터가 한 번 실행될 때마다, 인자로 받은 배열에 제네레이터의 결과를 추가한다. 함수 외부의 데이터를 변경시키는 사이드 이펙트가 발생하기 때문에 썩 좋은 설계라고 하기는 어렵다.
이 함수의 목적은 배열에 제네레이터의 결과를 저장하기 위함인데, 이렇게 변경한다면 순수성을 유지할 수 있을 것이다.
function pureCollect(generator)
{
let arr = [];
return function(...args)
{
let value = generator(...args);
if(value === undefined) return;
arr = [...arr, value];
return arr;
}
}
이 제네레이터 함수는 제네레이터를 지금까지 진행했던 제네레이터의 결과값으로 매핑시킨다. 결과적으로 제네레이터를 배열로 변환시키는 역할을 하긴 한다.
제네레이터를 순회할 때마다 새로운 배열 객체가 생성되어서 심각한 가비지 컬렉션을 일으킬 수 있지만, 적어도 순수성은 지킬 수 있다.
다른 방식으로 생각한다면, 배열로 축적한다는 것은 배열의 reduce 메소드와 비슷한 연산이라고 생각할 수도 있지 않을까? reduce와 비슷한 연산을 하도록 제네레이터를 확장해 보았다.
function reduce(generator, reducer, initial)
{
let accumulator = initial;
return function(...args)
{
let value = generator(...args);
if(value === undefined) return;
accumulator = reducer(accumulator, value);
return accumulator;
}
}
// 제네레이터의 값을 배열로 누적시키기 위해서는...
let harvester = reduce(generator, function(array, cur) {
array.push(cur);
return array;
}, []);
// 보다 엄밀한 순수성을 위해서는...
let pureHarvester = reduce(generator, function(array, cur) {
return [...array, cur];
}, []);
이 제네레이터 함수는 원본 제네레이터 함수를 리듀서를 적용시킨 누산 결과값으로 변환시킨다. 구체적으로는, 다음과 같은 동작을 한다.
이제 reduce 제네레이터는 보다 순수해졌으며, 비순수성의 도입은 온전히 개발자의 몫이 되었다.
위의 collect 팩토리 함수와 reduce 팩토리 함수의 바닐라 generator 문법 버전은 다음과 같다.
function* collect(generator, arr)
{
for(let item of generator)
{
arr.push(item);
yield item;
}
}
function* reduce(generator, reducer, initial)
{
let accumulator = initial;
for(let item of generator)
{
accumulator = reducer(accumulator, item);
yield accumulator;
}
}
function repeat(generator)
{
if(generator() === undefined) return;
return repeat(generator);
}
// 반복문 버전(더글라스 크락포드는 이 버전을 싫어할 것입니다)
function repeat(generator)
{
while(generator() !== undefined) {};
}
제네레이터는 순열이므로, 순열을 반복해야 한다. 이 함수는 제네레이터도, 제네레이터 팩토리도 아니지만, 제네레이터를 반복해서 완전히 소비시키는 함수다. 반환값은 없다.
저자는 반복문을 싫어하기에 재귀를 이용했는데, 그 중에서도 return문에 함수의 실행값을 집어넣는 꼬리 재귀를 사용하였다. 하지만, 애석하게도 대부분의 자바스크립트 엔진은 꼬리 재귀 최적화를 지원하지 않기에, 많은 양의 반복이 요구되는 제네레이터를 넣으면 스택 오버플로가 뜰 것이다.
바닐라 자바스크립트에서는 그냥 for of
문 써서 반복시키면 된다.
function harvest(generator)
{
const result = [];
repeat(collect(generator, result));
return result;
}
드디어 제네레이터를 배열로 만드는 함수가 등장했다. harvest 함수는 collect 함수와 repeat 함수의 조합으로, 제네레이터 함수가 소비되면서 result 배열에 그 결과가 저장되고, 완전히 소비되면 배열을 반환하는 형태다.
function harvest(generator)
{
const result = [];
const reducer = function(arr, value)
{
arr.push(value);
return arr;
}
repeat(reduce(generator, reducer, result));
return result;
}
이건 reduce 함수를 사용한 형태다. 배열을 선언한 뒤, 제네레이터의 값을 배열로 누적시킨 뒤 이를 반복하여 값을 반환한다.
바닐라 자바스크립트에서는 그냥 배열 구조 분해 할당을 사용하면 된다.
function limit(generator, count = 1)
{
let num = 0;
return function(...args)
{
if(num >= count) return;
num += 1;
return generator(...args);
}
}
제네레이터의 최대 호출 가능 횟수를 제한시키는 제네레이터를 생성한다. num이라는 내부 상태를 갖고, 제네레이터가 한 번 호출될 때마다 num의 값을 증가시킨다. 만약 미리 설정한 count보다 num 상태가 크거나 같으면 즉시 return하고, 그렇지 않다면 원본 제네레이터를 소비한다.
똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.
function* limit(generator, count = 1)
{
let num = 0;
for(let item of generator)
{
if(num >= count) return;
num += 1;
yield item;
}
}
function filter(generator, predicate)
{
return function filterGenerator(...args)
{
let value = generator(...args);
if(value === undefined) return;
if(predicate(value)) return value;
return filterGenerator(...args);
}
}
// 반복문 버전
function filter(generator, predicate)
{
return function(...args)
{
let value = generator(...args);
while(value !== undefined && !predicate(value))
{
value = generator(...args);
}
return value;
}
}
제네레이터와 predicate(술어 함수: boolean을 반환하는 함수)를 인자로 받아, 조건을 만족하는 제네레이터의 결과만 필터링하는 새 제네레이터를 생성한다. 이 제네레이터의 동작은 호출될 때마다 다음과 같이 이루어진다.
4번의 동작으로 인해, 제네레이터가 반환한 현재 값이 조건에 만족하지 않는 경우 그 단계를 스킵하는 효과가 있으며, 이는 조건에 만족될 때까지 계속 반복된다.
이것은 바닐라 자바스크립트의 제네레이터 문법으로 다음과 같이 구현할 수 있다.
function filter(generator, predicate)
{
for(let item of generator)
{
if(predicate(item)) yield item;
}
}
바닐라 제네레이터 문법으로 구현된 부분은 return filterGenerator(...args)
와 같이 재귀하는 부분에 대응하는 부분이 따로 보이진 않는데, for문과 조건문이 제네레이터를 소비하고, 조건에 맞지 않을 시 계속 반복하는 것을 내포하고 있기 때문이다.
function concat(...generators)
{
const generatorGenerator = element(generators);
let currentGenerator = generatorGenerator();
return function concatGenerator(...args)
{
if(currentGenerator === undefined) return;
let value = currentGenerator(...args);
if(value !== undefined) return value;
currentGenerator = generatorGenerator();
return concatGenerator(...args);
}
}
여러 개의 제네레이터를 인자로 받아, 제네레이터를 순차적으로 실행한다. 도중 제네레이터가 끝나면 다음 순서의 제네레이터를 실행한다.
이 제네레이터의 동작은 다음과 같이 이루어진다.
6번으로 인해, 제네레이터가 끝났다면 빈 제네레이터를 스킵하고, 비지 않은 제네레이터가 있을 조건에 맞을 때까지 함수를 반복시킨다.
function* innerGenerator()
{
yield 1;
yield 2;
yield 3;
}
function* outerGenerator()
{
yield "Hello!";
yield* innerGenerator();
yield "World!";
}
[...outerGenerator]; //["Hello!", 1, 2, 3, "World!"]
바닐라 자바스크립트의 제네레이터 문법에서는 yield*
라는, 다른 제네레이터에 결과를 위임하는 문법이 있다. 이걸 이용하면, 제네레이터 함수 내에서 다른 제네레이터의 결과물을 순차적으로 반환할 수 있다.
이를 활용하여, concat 제네레이터 팩토리 함수를 바닐라 자바스크립트의 제네레이터 문법으로 아주 쉽게 구현할 수 있다.
function* concat(...generators)
{
for(let generator of generators)
{
yield* generator;
}
}
참고로 yield*
문을 사용하지 않고는 이중 for of문을 이용하여 다음과 같이 구현할 수 있다.
function* concat(...generators)
{
for(let generator of generators)
{
for(let item of generator)
{
yield item;
}
}
}
function join(func, ...generator)
{
return function()
{
let consumed = generator.map( gen=>gen() );
if(consumed.every( value=>value===undefined )) return;
return func( ...consumed );
}
}
여러 개의 제네레이터와 한 개의 함수를 받아, 제네레이터를 한 번에 소비하고 소비된 값에 대한 함수의 결과를 반환하는 제네레이터를 생성한다. concat 제네레이터 팩토리가 제네레이터를 순차적으로 실행한다면, join 제네레이터 팩토리는 여러 개의 제네레이터를 병렬적으로 실행시키는 셈이 된다. 추가로, 제네레이터의 결과를 함수에 적용시키는 꼴이 되므로, 인자로 1개의 제네레이터를 넣는다면 마치 map과 같은 효과를 얻을 수 있다.
이 제네레이터는 실행될 때마다 다음을 수행한다.
1. 제네레이터 배열을 한 번에 실행하여, 제네레이터를 실행한 결과로 매핑한다.
2. 만약 모든 제네레이터 배열이 끝났다면, 제네레이터를 종료한다.
3. 제네레이터 실행 결과를 함수에 넣은 값을 반환한다.
1번으로 인해 여러 제네레이터를 동시에 소비할 수 있으며, 3번으로 인해 여러 제네레이터 함수가 func(...gen)
과 같은 형태로 매핑된다.
앞서 join 제네레이터 팩토리가 매핑의 역할을 한다고 말했으므로, 비슷하게 제네레이터를 매핑시키는 element, property를 join을 이용해 재구성할 수 있다.
function element(arr, generator = integer())
{
return join( (index)=>arr[index], generator );
}
function property(obj, generator=element(Object.keys(arr)))
{
return join( (key)=>[key, arr[obj]], generator );
}
이것은 바닐라 자바스크립트의 제네레이터 문법으로 다음과 같이 구현할 수 있다.
function* join(func, ...generator)
{
while(true)
{
let consumed = generator.map( gen=>gen.next() );
if(consumed.every( ({done})=>done===true ) ) return;
yield func( ...consumed.map( ({value})=>value ) );
}
}
바닐라 자바스크립트의 제네레이터 함수는 undefined가 묵시적인 끝으로 취급되는 클로저형 제네레이터와는 달리, undefined가 값이더라도 제네레이터가 끝이 아닐 수 있다.
function* permutation(arr, count, accumulator=[])
{
if(count <= 0) {
yield accumulator;
return;
}
for(let i=0; i<arr.length; i++)
{
const rest = arr.toSpliced(i, 1);
yield* permutation(rest, count-1, [...accumulator, arr[i]]);
}
}
자바스크립트의 제네레이터 문법 중 yield* 문을 적절히 활용하면, 재귀적인 제네레이터를 구현할 수 있다. 위의 코드는 arr 중 count만큼 뽑는 순열을 생성하는 제네레이터 코드인데, 동작은 다음과 같다.
[기존 accumulator, arr[i], permutation(rest, count-1)]
과도 같을 것이다.function permutation(arr, count)
{
if(count <= 0) return ()=>undefined;
if(count === 1) return join(value=>[value], element(arr));
const childPermutation = arr.map( (item, i)=>{
const semiPermutation = permutation(arr.toSpliced(i,1), count-1);
return join(value=>[item, ...value], semiPermutation);
} );
return concat(...childPermutation);
}
이것의 경우, yield*
문이 concat으로 변환되며, 조건에 맞는 자기 자신 제네레이터를 연결하는 것으로 해결할 수 있다.
위의 permutation 제네레이터가 아래의 코드로 어떻게 변환되었는지 알아보자.
function* permutation(arr, count)
{
if(count <= 0) return;
if(count === 1)
{
yield* arr.map( value=>[value] );
return;
}
for(let i=0; i<arr.length; i++)
{
const rest = arr.toSpliced(i, 1);
yield* map(permutation(rest, count-1), (value)=>[arr[i], ...value]);
}
}
내부적으로만 쓰면서 외부의 결과를 방해할 우려가 있는 accumulator는 제거되었으며, map 함수로 제네레이터를 변화시킴으로써 좀 더 재귀 제네레이터를 명확히 이해하게 되었다.
종료 조건도 변화되었는데, count가 0 이하일 경우에는 즉시 종료, count가 1일 경우에는 배열의 각 원소에 배열을 씌운 결과를 그대로 제네레이터 형태로 반환하도록 했다.
if(count <= 0) return;
의 경우, 제네레이터가 즉시 종료되므로, ()=>undefined
를 반환하도록 바꾼다.if(count === 1)
부분의 경우, 순회 가능한 arr을 value=>[value]
로 변환한 결과를 반환하고 있다. 이는, element(arr)에 value=>[value]
를 매핑한 결과로 변환할 수 있다. if(count === 1) return join(value=>[value], element(arr));
로 변환한다.yield* map(permutation(rest, count-1), (value)=>[arr[i], ...value]);
를 반복해서 반환하는 것에서 볼 수 있듯이, 제네레이터를 순차적으로 실행한다. 우리는 이걸 concat
팩토리 함수를 이용해 바꿀 것이다.map(permutation(rest, count-1), (value)=>[arr[i], ...value])
이라는 결과가 각각의 배열의 원소에 대해 반복되고 있으므로, 원래 배열에 대해 map 함수를 이용하여 제네레이터를 생성한다.permutation(arr.toSpliced(i,1), count-1)
은 제네레이터다. 여기에, 제네레이터를 매핑하는 join 함수를 이용해 arr을 join(value=>[item, ...value], semiPermutation)
으로 매핑해준다.