Generator
- 우리가 직접 iterable인 객체를 만들 수는 없을까요?
- iterable protocol을 구현하기만 하면 어떤 객체든 iterable이 될 수 있음
- iterable을 구현하는 가장 쉬운 방법은 ES2015에 도입된 generator()를 사용하는 것
- Generator()는 iterable 객체를 반환하는 특별한 형태의 함수.
function* gen1() {...}
const gen2 = function* () {...}
const obj = {
* gen3() {...}
}
- generator()를 호출하면 generator객체가 생성되는데, 이 객체는 iterable protocol을 만족함. 즉 Symbol.iterator 속성을 갖고 있다.
function* gen1() {
}
const iterable = gen1();
iterable[Symbol.iterator];
- generator() 안에서는 'yield'라는 특별한 키워드 사용가능
- generator() 안에서 'yield'키워드는 'return'과 유사한 역할을 함.
- iterable의 기능을 사용할 때 'yield'키워드 뒤에 있는 값들을 순서대로 반환해 줌.
function* numberGen() {
yield 1;
yield 2;
yield 3;
}
for (let n of numberGen()) {
console.log(n);
}
- yield* 표현식을 사용하면, 다른 generator() 넘겨준 값을 대신 넘겨줄 수 있음.
function* numberGen() {
yield 1;
yield 2;
yield 3;
}
function* numberGen2() {
yield* numberGen();
yield* numberGen();
}
for (let n of numberGen2()) {
console.log(n);
}
- genenerator()는 일반적인 함수의 내부 동작 방식과 별차이가 없습니다. 차이점 yield 키워드뿐.
- 다음은 generator()를 이용한 예들
function* range(start = 0, end = Infinity, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
function* fibonacci(count = Infinity) {
let x = 1;
let y = 1;
for (let i = 0; i < count; i++) {
yield x;
[x, y] = [y, x + y];
}
}
function* repeat(item, count = Infinity) {
for (let i = 0; i < count; i++) {
yield item;
}
}
function* repeatMany(array) {
while (true) {
for (let item of array) {
yield item;
}
}
}
generator() 사용시 주의할 점
- generator()로부터 생성된 iterable은 한 번만 사용가능
- generator() 내부에서 정의된 일반 함수에서는 yield 키워드를 사용못함.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iter = gen();
for (let n of iter) {
console.log(n);
}
for (let n of iter) {
console.log(n);
}
function* gen2() {
function fakeGen() {
yield 1;
yield 2;
yield 3;
}
fakeGen();
}
Generator와 Iterator
- generator()로부터 만들어진 객체는 일반적인 iterable처럼 쓸 수 있지만 iteraoter와 관련된 특별한 성질을 갖고 있음
- generator()로부터 만들어진 객체는 iterable protocol과 iterator protocol을 동시에 만족함.
function* gen() {
}
const genObj = gen();
genObj[Symbol.iterator]().next === genObj.next;
- 즉, Symbol.iterator를 통해 iterator를 생성하지 않고도 바로 next를 호출할수 있음.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iter = gen();
iter.next();
iter.next();
iter.next();
iter.next();
- generator() 안에서 return 키워드를 사용하면 반복이 바로 끝나면서 next()에서 반환된 객체의 속성 앞의 반환값이 저장됨.
- 다만 return을 통해 반환된 값이 반복 절차에 포함되지는 않음.
function* gen() {
yield 1;
return 2;
yield 3;
}
const iter = gen();
iter.next();
iter.next();
iter.next();
for (let v of gen()) {
console.log(v);
}
- generator()로부터 생성된 객체의 next()에 인수를 주어서 호출하면, generator()가 멈췄던 부분의 yield 표현식의 결과값은 앞에서 받은 인수가 됨.
function* gen() {
const received = yield 1;
console.log(received);
}
const iter = gen();
iter.next();
iter.next('hello');
- generator() 이런 성질은 비동기 프로그래밍을 위해 활용되기도 함.
generator 활용
function* map(iter,mapper) {
for(const v of iter) {
yield mapper(v)
}
}
function* filter(iter, test) {
for(const v of iter) {
if(test(v)) yield v;
}
}
function* take(n, iter) {
for(const v of iter) {
if(n<=0) return;
yield v;
n--;
}
}
const values = [1,2,3,4,5,6,7,8,9,10];
const result = take(3,map(filter(values, n=>n%2 ===0), n=>n*10));
console.log([...result]);
function* map(iterable, mapper) {
for (let item of iterable) {
yield mapper(item);
}
}
function* reduce(iterable, reducer, initial) {
let acc = initial;
for (let item of iterable) {
acc = reducer(acc, item);
yield acc;
}
}
function* filter(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* concat(iterables) {
for (let iterable of iterables) {
yield* iterable;
}
}
function* take(iterable, count = Infinity) {
const iterator = iterable[Symbol.iterator]();
for (let i = 0; i < count; i++) {
const {value, done} = iterator.next();
if (done) break;
yield value;
}
}