비동기성과 반복을 위한 코루틴을 추상적 관점에서 다루며, “기본 경우”를 함께 놓고 보면 `AsyncIterator`를 `Iterator`와 `Future`의 곱으로 이해할 수 있다는 점을 설명한다. ASCII 다이어그램과 타입 테이블을 통해 각 개념의 관계를 풀이한다.
이전 글(https://without.boats/blog/poll-next)에 이어, 비동기성과 반복을 위한 코루틴의 사용을 보다 추상적인 관점에서 조금 더 풀어보는 짧은 메모를 남기고 싶었다. AsyncIterator가 Iterator와 Future의 곱(product)이라는 내 주장은, 비동기도 반복도 아닌 코드 블록이라는 “기본 경우(base case)”까지 함께 고려하면 더 잘 와닿는다는 것을 깨달았다.
또 하나의 재미있는 ASCII 다이어그램을 그릴 구실이기도 하고, 애써 마련한 Berkeley Mono 라이선스도 제대로 써먹어야 하지 않겠나.
여기서 “기본 경우”란 그냥 평범한 블록을 뜻한다. 물론 이것은 다른 것들과 같은 의미의 “코루틴”은 아니다. 러스트에서는 즉시 평가되니까. 하지만 즉시 평가되지 않는 지연(lazy) 블록을 가진 언어를 상상해볼 수는 있다. 지연 평가 이야기에 너무 깊이 들어가고 싶지는 않다. 레딧 어딘가에서는 분명 하스켈이니 모나드니, 러스트의 설계가 치명적으로 결함이 있다는 얕은 댓글이 달릴 테고, 그런 의견을 갖는 건 그들의 자유다. 그렇지만 지연과 즉시 의미론의 구분을 잠시 잊고 보면, 평범한 블록도 일종의 코루틴으로 볼 수 있다. 항상 준비되어 있고(비동기 아님), 단 한 번만 평가되는(반복 아님) 그런 코루틴 말이다.
이렇게 보면 두 개의 축, 즉 비동기성과 반복으로 이루어진 도표를 얻을 수 있고, 기본 경우에서 시작해 어느 경로로 가더라도 AsyncIterator에 도달하는 모습을 볼 수 있다:
A S Y N C H R O N O U S
──────────────────────── ▶
╔═══════════════╗ ╔═══════════════╗
║ ║░░ ║ ║░░
║ BASE CASE ║░░ ║ FUTURE ║░░
║ ║───────────── ▶ ║░░
│ ║ { } ║░░ ║ async { } ║░░ │
I │ ║ ║░░ ║ ║░░ │ I
T │ ╚═══════════════╝░░ ╚═══════════════╝░░ │ T
E │ ░░░░░░│░░░░░░░░░░ ░░░░░░│░░░░░░░░░░ │ E
R │ │ │ │ R
A │ │ │ │ A
T │ │ │ │ T
I │ │ │ │ I
V │ ╔═══════▼═══════╗ ╔═══════▼═══════╗ │ V
E │ ║ ║░░ ║ ║░░ │ E
▼ ║ ITERATOR ║░░ ║ ASYNCITERATOR ║░░ ▼
║ ║───────────── ▶ ║░░
║ gen { } ║░░ ║ async gen { } ║░░
║ ║░░ ║ ║░░
╚═══════════════╝░░ ╚═══════════════╝░░
░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░
──────────────────────── ▶
A S Y N C H R O N O U S
이러한 관계는 이전 글의 타입 표에 기본 경우를 추가해 보면 더욱 분명해진다:
│ YIELDS │ RETURNS │ RESUMES
──────────────┼─────────────────────┼─────────────────┼─────────────────
│ │ │
BASE CASE │ ! │ Self::Output │ ()
│ │ │
FUTURE │ () │ Self::Output │ &mut Context
│ │ │
ITERATOR │ Self::Item │ () │ ()
│ │ │
ASYNCITERATOR │ Poll<Self::Item> │ () │ &mut Context
│ │ │
보면, 기본 경우와 Future는 동일한 반환 타입을 갖고, 기본 경우와 Iterator는 동일한 재개 타입을 갖는다. AsyncIterator는 Iterator와 Future를 모두 결합한다. 그리고 산출(yield) 타입도 곰곰이 보면 같은 진행 단계를 드러낸다. 기본 경우의 산출 타입은 !, 즉 비어 있는(uninhabited) 타입이다(평범한 블록은 결코 yield 하지 않는다). 이에 비해 Future는 Pending을 표현하기 위해 단위 타입 ()을 산출하고, Iterator는 아이템 타입을 산출한다. 그러면 AsyncIterator는 Future와 Iterator의 합(sum)을 산출하는 셈이 된다. 즉 아이템 타입이거나, 보류(pending) 상태이거나 둘 중 하나인 타입이다.