Generator로 만든 이터러블과 for...of가 만났을 때
— Functional programming — 1 min read
프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 질문/답변에 대한 추가학습
# 문제점
보조함수가 프로미스일때, coll로 객체를 받지 못함
1reduce(2 (a, b) => Promise.resolve(a + b),3 Promise.resolve({a:1,b:2,c:3})).then(console.log) //3 --> 문제있음4 5reduce(6 (a, b) => Promise.resolve(a + b),7 {a:1,b:2,c:3}).then(console.log) //3 --> 문제있음8 9reduce(10 (a, b) => Promise.resolve(a + b),11 [1,2,3]).then(console.log) //6 --> 정상동작12 13reduce(14 (a, b) => a + b,15 Promise.resolve({a:1,b:2,c:3})).then(console.log) //6--> 정상동작
보조함수가 프로미스 이면, for문의 acc = f(acc, v)
의 값은 프로미스이기 때문에 재귀가 일어난다.
이 때,
coll
이 "배열/배열담은 Promise"일 땐, 재귀를 돌렸을때 for문이 그 자리에서 다시 진행되지만coll
이 "객체/객체담은 Promise"일 땐, 재귀를 돌렸을때 for문이 종료되어있다
-> for루프는 종료된 상태이기 때문에 for루프를 1회밖에 순회하지 못한 acc(누적값)이 반환값이 된다
for루프가 종료되어 있는 이유는 무엇일까?
- 배열 역시 제너레이터로 만든 객체이면 오작동한다
->valuesIter()
제너레이터의 문제인 것 같다
1var obj = {a:1,b:2,c:3}2var objIterG = valuesIter(obj)3objIterG // valuesIter {<suspended>}4
5var arr = [1,2,3]6var arrIterG = valuesIter(arr);7arrIterG // valuesIter {<suspended>}8
9reduce6((a, b) => Promise.resolve(a + b), arrIterG).then(console.log) 10// 3 --> 오작동11reduce6((a, b) => Promise.resolve(a + b), objIterG).then(console.log)12// 3 --> 오작동
# 문제 원인 찾기
Generator의 기본동작
- 제너레이터는 제너레이터 객체를 반환한다
- 제너레이터 객체는 for-of 루프로 순회할 수 있으며 = 이터러블(iterable)이면서
- next() 메소드를 가지고 있다 -> 동시에 이터레이터(iterator)이다
- 제너레이터 함수는 호출되어도 즉시 실행되지 않고, 대신 함수를 위한 Iterator 객체(일종의 pointer)를 반환한다
- Iterator의 next메서드를 호출하면 제너레이터 함수가 실행되어 yield문을 만날 때마다 value, done 프로퍼티를 갖는 객체를 반환한다
Generator.prototype.return()
제너레이터의 .return()
메소드는 제공된 값을 반환하고 Generator를 종료시킨다
1var test = valuesIter([1,2,3])2test.next(); // {value: 1, done: false}3test.next(); //{value: 2, done: false}4
5test // valuesIter {<suspended>} --> 아직 종료되지 않은상태6
7test.return(); //{value: undefined, done: true} --> 종료시키기8
9test3 // valuesIter {<closed>} --> 종료된상태
Array Iterator vs Generator
- 일반 이터러블객체의 iterator에는
.return()
메소드가 없지만 - 제너레이터로 만든 제너레이터객체의 iterator에는
.return()
메소드가 있다
1var arrIter = [1,2,3][Symbol.iterator](); // Array Iterator {}2var arrIterG = valuesIter([1,2,3]); // valuesIter {<suspended>}3
4arrIter.next(); // {value: 1, done: false}5arrIter.return(); // TypeError -> return메소드 없음6
7arrIterG.next(); // {value: 1, done: false}8arrIterG.return(); // {value: undefined, done: true}
Array Iterator
Generator Iterator
Generator로 만든 이터러블과 for...of가 만났을때
for...of는 내부적으로 루프가 종료되었을 때, 받아둔 iterator에
.return()
메소드가 있다면 이를 실행시켜 이터러블을 종료시킨다
reduce()
의 보조함수로 Promise가 왔을때, 비동기가 일어나 재귀를 돌게되는데...
일반 이터러블의 iterator엔
.return()
메소드가 없어 재귀를 돌기위해 루프를 빠져나가도 다시 그 자리에서 루프가 실행되지만제너레이터로 만든 객체의 iterator는 재귀를 돌기위해 루프를 빠져나가는 순간,
.return()
메소드를 실행시켜 제너레이터를 종료시키므로 더이상 순회가 불가능해진다for...of 는 내부적으로 for of 가 종료된 후에 받아둔 iterator에
.return()
메소드가 구현되어있다면.return()
을 실행하도록 되어있다그래서 acc값이 프로미스라 재귀를 돌기위해
return acc.then(recur);
으로 for...of를 빠져나가면, iterator의.return()
을 실행하므로 Generator가 종료된다
1//1. coll의 값으로 plain object가 오면 2
3//2. *valuesIter 제너레이터를 이용해 iterable 객체를 만든다4
5const collIter = coll => 6 coll.constructor == Object ? valuesIter(coll) : coll[Symbol.iterator]();7
8function reduce(f, coll, acc) { 9 return then(function () {10 var iter = collIter(coll);11 acc = acc === undefined ? iter.next().value : acc;12 return then(function recur(acc) {13 for (const a of iter) {14 acc = f(acc, a); 15 //3. 보조함수가 프로미스이기 때문에, 반환값은 프로미스 -> acc16 17 if(acc instanceof Promise) return acc.then(recur);18 //4. 재귀를 돌기위해 for...of를 빠져나가면19 //5. iterator의 `.return()`을 실행 -> Generator 종료20 }21 return acc; 22 //6. 더이상 제너레이터 객체 순회 불가하기 때문에, 첫 누적값이 반환23 }, acc)24 }, coll)25}26
27reduce((a, b) => Promise.resolve(a + b), {a: 1, b: 2, c: 3}); // 3
- 비동기가 안일어나면,
제너레이터로 만든 이터레이터를 for...of에 다시 넣어도.return()
이 실행되기전에 재귀가 먼저 종료되므로 문제가 안생기고 - 비동기가 일어나면,
재귀를 돌기위해 for문을 빠져나가면서.return()
이 먼저 실행되어,iter.next()
가{ done: true }
가 되도록 되어있어 더이상 for...of로 순회가 불가능하게된다
Q: 왜 이런현상이 생길까요 ?
A: 에러가 발생했을때 이터레이터를 종료시켜서 제너레이터 객체 자체를 언능 날려버릴 수 있게 하기 위한 목적으로 보여집니다
# 해결책
- 제너레이터로 만든 결과를
.return()
이 없는 객체로 감싸서 리턴 하거나 - 파격적으로
.return
에null
을 대입해버리는 방법이 있다
1const iter = valuesIter({ a: 1, b: 2 });2
3//1.4function wrap(iter) {5 return { next: () => iter.next(), [Symbol.iterator]() { return this; }};6}7
8//2.9function delReturn(iter) {10 iter.return = null;11 return iter;12}
제너레이터로 생성할 이터러블을 gen함수로 감싸주자
1function gen(g) {2 return function(v) {3 const iter = g(v);4 return { next: () => iter.next(), [Symbol.iterator]() { return this; } }5 }6}7
8const valuesIter = gen(function *(obj) {9 for (const k in obj) yield obj[k];10});