JavaScriptのyield*は移譲先ジェネレーターがreturnした値はyieldしない

本記事は https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/yield*#%E4%BE%8Byield* 式自体の値 に例示されているものを改めて考えつつ説明したものです。

https://github.com/susisu/tskaigi2025 のコードリーディングをしていて、ここがわからなくてしばらく詰まっていた。

it("ジェネレータから yield された Promise が fulfill されたらその位置から再開する", async () => {
  function* myFunc(): Comp<number> {
    const a = yield* waitFor(Promise.resolve(1));
    const b = yield* waitFor(Promise.resolve(2));
    return a + b;
  }
  const promise = run(myFunc());
  await expect(promise).resolves.toEqual(3);
});

JavaScriptには yield* という構文がある。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/yield*

これはジェネレーターの中で使い、別のiterableに制御を移譲するために使う。とてもわかりやすい例があったので引用させてもらう。
https://stackoverflow.com/questions/17491779/delegated-yield-yield-star-yield-in-generator-functions

function* someGenerator() {
    yield 0;
    yield [1, 2, 3];
    yield* [4, 5, 6];
}

for (v of someGenerator()) {
    console.log(v);
}
// 0, [1, 2, 3], 4, 5, 6

yield [1, 2, 3];[1, 2, 3] をそのままyieldする。一方で yield* [4, 5, 6];[4, 5, 6] がiterableなオブジェクトならば、その生成値を1つずつyieldする。移譲すると書いたが、someGeneratorの呼び出し側から見ると、内側の内側にあるiterable([4, 5, 6] のこと)を透過的に扱えるとも言えそうだ。

ところでジェネレーターの中ではyieldの他にreturnも使える。returnに到達すると値を返しつつジェネレーターは終了する。

const gen = function* () {
  yield 1;
  return 2;
}()

console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: true, value: 2 }
console.log(gen.next()); // { done: true, value: undefined }

では yield* で移譲されたジェネレーターがreturnしたとき何が起きるのか。僕はなんとなくreturnされた値も透過的に外側から触れると思っていたが、触れない。

const g_inner = function* () {
    yield 1;
    return 2;
}();

const g_outer = function* () {
    const a = yield* g_inner;
    yield 3;
    return 4;
}()

console.log(g_outer.next()); // { done: false, value: 1 }
console.log(g_outer.next()); // { done: false, value: 3 }
console.log(g_outer.next()); // { done: true, value: 4 }

なんとなく出てくる値は 1, 2, 3, 4 になりそうな気がしたが(俺だけか?)、正解は1, 3, 4だ。MDNに

yield* は式であり、文ではありません。そのため、値に評価されます。

とサラリと書かれている通り、g_innerのreturnで返される値はg_outerのaに束縛される。それだけで、g_outerの呼び出し側には返されない。

ここで逆に考えてみる。移譲先のreturnで停止すると仮定すると、どのような問題が起きるだろうか?

まず、yieldの機能として、呼び出し側からnextに引数を与えることで、値を受け渡すことができる。

const gen = (function* () {
  const a = yield 1;
  const b = yield 2;
  return a + b;
})();

console.log(gen.next());     // { value: 1, done: false }
console.log(gen.next(10)); // { value: 2, done: false }
console.log(gen.next(20)); // { value, 30, done: true}

初回呼び出し後にyield 1;で停止しているジェネレーターに対して10を与えて再開させ、次にyield 2;で停止しているジェネレーターに20を与えて再開させている。3回目のnext呼び出しの後はreturn a+bに到達して30を返して終了する。

これは yield* で移譲された内側のジェネレーターにも機能するのだが、returnで停止すると仮定してシミュレーションしてみる。

const g_inner = function* () {
    const a = yield 1; // aに10が入る
    return 2; // ここで停止すると仮定
}();

const g_outer = function* () {
    const x = yield* g_inner;
    yield 3;
    return 4;
}()

console.log(g_outer.next());     // { done: false, value: 1 }
console.log(g_outer.next(10)); // { done: false, value: 2 } が返ると仮定する
console.log(g_outer.next(20)); // { done: false, value: 3 } このnextに渡した20はどこへ…?
console.log(g_outer.next());     // { done: true, value: 4 }

return 2で停止すると仮定すると、そのときnextに渡された値はどこに行くのだろう?xにはg_innerから返された値が入ることになっているので行き場がない。捨てるしかない。そうなると、g_outerを利用する側は中でg_innerに処理を移譲しているなんて本来知る必要がないのに、それを知らないと何回目かのnextに渡した値が勝手に捨てられてしまうことになる。インターフェースとしての安定性が失われている。

そういう意味で、returnでは停止しないのは筋が通っている。割と非直感的だと思ったんだけど、MDNだとサラッと1行説明されて例が1つあるだけで終わるので難しかった。普段からジェネレーターを使ってる人(いますか?)には自明な動作なのかもしれない。

一般のジェネレーターにおいてreturnは停止して値を返すという点でyieldと同じように見えるが、その見方は間違っているのだろう。yieldが明確に停止を意図した機能である一方で、returnは終了だから結果的に停止に見えてしまっているだけで、その本質はただ値とともに処理を返すことだけだ。yield* のようにその後も外側のジェネレーターに制御を戻して続行できるのであれば停止しないように見えるのは必然なのだろう。

20250530 褒められ欲

項目 内容 得点 換算点
睡眠時間 6時間26分 77 10.0/13.0
起床 7:19 100 8.0/8.0
散歩 実施・ゴミ拾いなし 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 ノー 0 0.0/5.0
労働 passion: 80点, discipline: 75点 78 18.7/24.0
ジム 休養日 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 実施 100 7.0/7.0
あすけん - 60 5.4/9.0
総合 1日の総合評価 - 83

今週はTSKaigiのモチベバフと睡眠好調が相まってかなりモーレツ気味(当社比)で働いていたけど、金曜日はさすがに疲労を感じる。意外と体は習慣通りに動きそうなんだけど、精神力が上がりきらない。

https://github.com/susisu/tskaigi2025

これがめっちゃ難しくてClaudeに解説させながら読んでる。AI教師、対応範囲が(人間よりは)広いし能力は間違いないし時間も場所も感情も気にする必要ないし優しいのでかなりすごい。AIに褒められて褒められ欲(結構ある)の充足を感じるけどこれでいいのかなあと疑問。でも確実に僕の役に立っているしなんなら僕を賢くしている。

明日は久々の完全オフ。ジム以外特にすることなさそうなのでのんびり布団でも干すかな。うまいものも食いたい。食いに行きたい。休養だけでなくリフレッシュの気持ちになっているのもモーレツの反動かもしれない。

20250529 『モナ王』/sort-package-json3.2.0でソートアルゴリズムが変わったっぽい

項目 内容 得点 換算点
睡眠時間 7時間19分 100 13.0/13.0
起床 8:08 93 7.4/8.0
散歩 実施・ゴミ拾いあり 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 実施 100 5.0/5.0
労働 passion: 85点, discipline: 75点 80 19.2/24.0
ジム 有酸素 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 実施 100 7.0/7.0
あすけん - 80 7.2/9.0
総合 1日の総合評価 - 93

今日の仕事は並列度が高くてあまり集中できなかったな。

『モナ王』

『モナ王』を食べた。『チョコモナカジャンボ』の隣に並んでいたらまず選ばない商品だが、だからこそ食べておくかということで食べた。結論から言うと特に選ぶ理由はない。チョコの風味が要らない、パリパリが要らない、そんなムーディーな気分の日ならいいかもしれない。

sort-package-json3.2.0でソートアルゴリズムが変わったっぽい

sort-package-jsonというライブラリがある。その名の通りpackage.jsonをソートすることで秩序をもたらすツールである。最近npmのソートアルゴリズムに合わせるということで変更が入ったのだが

https://github.com/keithamus/sort-package-json/pull/358

これがyarnのソートアルゴリズムとは違っており、しかも間の悪いことにyarnは yarn install を実行するたびにpackage.jsonをソートし直すので、sort-package-jsonとの共存が実質的に不可能である(開発中に yarn install をするたびに意味のない差分が発生し、commitするときにlint-stagedで消える)。じゃあsort-package-jsonの代わりに yarn install を使えばいいのでは?とも思うが、コマンドの意味・副作用からしてかなり正しくない度が高い解決だ。苦しい。

https://github.com/keithamus/sort-package-json/issues/363

ソートアルゴリズムの違いというのはどうやらこういう違いのようだ。

https://github.com/keithamus/sort-package-json/issues/355#issuecomment-2848325132

// yarn
> ["a", "b", "A", "B"].sort()
[ 'A', 'B', 'a', 'b' ]
// npm, sort-package-json
> ["a", "b", "A", "B"].sort((a,b) => a.localeCompare(b, "en"))
[ 'a', 'A', 'b', 'B' ]

実はnpmではパッケージ名に大文字を使ってはいけない。昔は使えたのかも知れないが、今は大文字名でinstallしようとすると404で弾かれる。だからこの問題は内製パッケージを使っていたりエイリアスを付けていたりしないと表面化しにくい(一応 -_ とかでも差が出てしまうらしい?)。

まあそこまで重大な違いではないように思えるが、sort-package-jsonは結構な人気があるパッケージなので、そこそこ困っている人がいるかもしれない。

20250528 一生は長いが一日は短い

項目 内容 得点 換算点
睡眠時間 6時間53分 95 12.3/13.0
起床 7:37 100 8.0/8.0
散歩 実施・ゴミ拾いなし 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 実施 100 5.0/5.0
労働 passion: 90点, discipline: 85点 88 21.1/24.0
ジム 有酸素+筋トレ 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 実施 100 7.0/7.0
あすけん - 65 5.8/9.0
総合 1日の総合評価 - 93

今日もかなり良い。やはり睡眠が基本だ。朝は散歩しながらプログラミングのフローについて考えていた。TypeScriptを第一言語とする僕にとっては、関数の連続は

const a = f1();
const b = f2(a);
const c = f3(b);

のように書かれた方がわかりやすい。

const c = f3(f2(f1()));

は厳しい。上から下に流れること、得た値を束縛すること。この2点がだいぶ馴染みがある感に寄与している。手続き的と宣言的と言ってもいいのかもしれない。あるいはメソッドを生やしてやって

const c = f1().then(f2).then(f3);

という考え方もあるかもしれない。同じプログラムの流れであっても、それがどこに表現あるいは隠蔽されているのかというのがいろいろ違うよねという話。そして流れの理解が重要なプログラムを書くときに、流れを隠蔽するような記法を使って良いのか?

これがまあ f3(a, b) になったりPromiseになったりResultになったりするとどんどんゴチャってきて、その場合どれがベストなのか…!という話になる。言語がデザインする書き方もあるし、個人・チームの慣れ・志向もある。

人生が長いか短いかはまだわからない(どちらかと言えば長い寄りの感覚でいる)が、一日は間違いなく短いということを最近感じる。ちゃんと寝て、起きて働いてやるべきことやって、その上で遊びの時間も取るというのは、かなりのテクと意思が必要だ。31歳なのでね。幸いやりたいことが減ってくれて、集中できる。

20250527 キユーピー

項目 内容 得点 換算点
睡眠時間 6時間58分 99 12.9/13.0
起床 7:42 100 8.0/8.0
散歩 実施・ゴミ拾いなし 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 ノー 0 0.0/5.0
労働 passion: 90点, discipline: 85点 88 21.1/24.0
ジム 休養日 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 実施 100 7.0/7.0
あすけん - 43 3.9/9.0
総合 1日の総合評価 - 87

今日は結構いい日だったな。何より睡眠が良く、それに伴って労働も良い。良いこともあった。一日の中でやりたいことが減ってきて、残り少なくなったやりたいことに集中できる感覚がある。

スーパーで新商品のメロンパンがあってつい買って食べてしまった。いつも通り、メロンパンってそこまでメロンでもないよねという感想。野菜を食べるご褒美としてドレッシングはちょっと良いやつを買っていて、今日はキユーピーテイスティドレッシングイタリアン。ちょっと良いかもしれない…?なんか油ばっかり出てきて撹拌が足りなかった気がする。ところで今コピペして気づいたんだけど大きいユが正しいの?

https://www.kobe-np.co.jp/rentoku/omoshiro/202303/0016174625.shtml

デザイン上の理由らしい。へぇ〜

なんか今日はGitHub Copilot経由のClaudeやGeminiの利用がよく「申し訳ございません。応答が返されませんでした」で止まる。Copilot Chatの出力を見ると503でrate limitのせいらしいが、そこまで酷使してないぞ…?よくわからないな。計算資源の取り合いは激しくなっていく(今はユーザー囲い込みのためにどこも出血大サービスしてる価格のはず)だろうから、コスパのいいモデルが進歩してほしいなあ。まあ必ずしもベストなモデルを使う必要はなく、今はClaude4に利用が殺到しているだろうけど別に3.7や3.5でもそこそこやれるんだよな。

20250526 Carbohydrates

項目 内容 得点 換算点
睡眠時間 6時間25分 77 10.0/13.0
起床 7:49 100 8.0/8.0
散歩 実施・ゴミ拾いあり 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 ノー 0 0.0/5.0
労働 passion: 90点, discipline: 90点 90 21.6/24.0
ジム 有酸素 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 実施 100 7.0/7.0
あすけん - 53 4.8/9.0
総合 1日の総合評価 - 85

いくつかのルートでゴミ拾いして、どこが汚いかなんとなくわかってきた。掃除をする人間がいる道といない道があるんだよな。

昨夜TLで家系ラーメンの画像を見せられてしまい、勢いで昼に家系ラーメンを食べてしまった。猛烈に体に悪い一方でそこまでうまいわけでもなく、炭水化物の量を喜べる年齢でもなく、なんだかなあという感想で終わってしまった。

ジムは本来有酸素だけの日だったが隠れ肥満脱却のために、余力がありそうな腹筋はやった。

ジム中も風呂中もずっと仕事のことを考えていた。TSKaigiでモチベが上がっている。単に仕事をこなしていてもTypeScriptの極みには至れないと思うけど、僕が働きながらそこに近づくには、やはり仕事の延長線上で頭を使い続けることだと思うんだよな。食っていく必要もあるので。この年になると他に考えるべきこともないしな。

20250525 新垣ちんすこうアイス

ゆっくり起きて朝食。のんびりTSKaigiの内容を振り返り、午後はジム。全身ガッツリやって相撲の最後だけ見て買い物して帰宅。

豊昇龍強かったね。パワーでは大の里が当然勝っていたけど、右で廻しを取りたい大の里に対して徹底してそれを切り、半時計周りに振り回しながら得意の左への投げの形に持ち込んだ。クレバーな運びだった。

『新垣ちんすこうアイス』を食べた。どこにもそう書いてはいないけどどことなくパッケージの佇まいがスーパーカップで、味もスーパーカップをベースにしてサクサクのちんすこうがアクセントになっている感じだった。調べてみると作っているのは沖縄明治乳業であり、明治乳業の関連会社なのでまあそういうことなのかもしれない。

昼寝。

金・土とTSKaigiに出ていたので今日は休養と家事に専念した。明日からはより一層スピードアップして働きたい。いや俺がスピードアップするんじゃなくてプロダクトを進化させていきたいね。

20250524 TSKaigi 2025 Day 2

TypeScriptネイティブ移植観察レポート TSKaigi 2025

https://2025.tskaigi.org/talks/berlysia

https://speakerdeck.com/berlysia/typescript-native-porting-observation-tskaigi-2025

tsgoの歩みについてのレポート。コンテキストから細部までよくわかる話だった。Compiler APIどうなっちゃうんだろうなあ。

フロントエンドがTypeScriptなら、バックエンドはPHPでもいいじゃない

https://2025.tskaigi.org/talks/hanhan1978

https://speakerdeck.com/hanhan1978/php-is-not-bad

TSKaigiとしては異色の発表。ベテランバックエンドエンジニアの視点から、バックエンド/フロントエンドの歴史について振り返る。後半はPHPの圧倒的なシェアの高さを見せた上でのPHPはいいぞという話。トーク力が素晴らしく、楽しみながら頭に入ってくる発表だった。

Pragmatic Functional Programming in TypeScript

https://2025.tskaigi.org/talks/_yasaichi

TSのプロジェクトにFPを導入したいということはよくあるが、チームとしてそれを採用する判断ができるかは難しい。まず単純化した5つの原則とその嬉しさを把握し、実利ベースで導入を進めていこうというのがPragmaticということ。

君だけのオリジナル async / await を作ろう

https://2025.tskaigi.org/talks/susisu2413

https://speakerdeck.com/susisu/tskaigi-2025

ジェネレータにおけるyieldを文脈から値を取り出す処理?と捉えて、async/awaitのみならずResult、エフェクトシステムまで実装してしまう話。とても難しいが興味のある分野なのでよく読み直したい。プログラミング理解の足腰が強い人は「結局こういうことなんだよね」と抽象化して裏返して思いもよらない使い方を編み出したり、足りない場所に気づいて埋めに行ったりする。基礎力だなあ。

TS特化Clineプログラミング

https://2025.tskaigi.org/talks/mizchi

https://tskaigi.mizchi.workers.dev

効くプロンプトとうまくいかないプロンプトの事例集。AIエージェントを使い倒してる発表者ならではの、AIとはどのような学習の結果何ができて何ができないのか考察が深い。

OST (Open Space Technology)

参加者がテーマごとの10のグループに別れて自由にディスカッションする企画。僕はフロントエンドのディレクトリ構造のグループに参加した。自分たちのチームが採用している構造についてあまりうまく話せなかったのは後悔が残るところだが、いろいろな人の重視するポイントが聞けて面白かった。

総括

TypeScriptの領域は広く深く、仕事でwebアプリケーションを開発してるだけでは浅瀬もいいところなんだなと感じた。いろいろな分野で非常に深く研究している人がいてかっこいいなあ。久しく忘れていたすごいエンジニアはカッコいいという感情を思い出した。そう思うと同時に、いくらやっていきを得ても全部ガチるのは無理なので、どこに軸足を置いてやっていくかを冷静に見極める必要がある。現状のプログラミングに満足しちゃいけないな。もっと楽にできる、もっと安全にできるという強い気持ちを持つべきだ。

2日間ガッツリ参加して学ぶというのはそこそこ大変。移動も大変。弁当は美味しかった。いろいろな縁がある人に会えたり新たに縁が生まれたりというのは物理会場ならではの良さだったな。

20250523 TSKaigi 2025 Day 1

n円払って有給取って行ってきました。

SignalとObservable―新たなデータモデルを解きほぐす

https://x.com/laco2net

https://docs.google.com/presentation/d/1c7fYqn7-v3hnbKtmwXHbvwuCs6cNq_ThLKVDZ7rvbA0/preview?slide=id.g260298bad6d_0_77

UI開発における状態管理の話に始まり、歴代の状態管理ライブラリを概観しつつ「結局やりたいのって状態+派生状態+イベントドリブンな副作用なんだよね」と整理する(mobxが言及されて嬉しい)。さらにそれを「値の生産」というレベルにまで抽象化し、見慣れた同期/非同期・単一/複数という軸に新たにpull/pushという軸を加えることで、Signalの話とObservableの話はFunctionやIterator、Promiseから全部つながっていたんだと最後に納得させられる大回転。

すごいことをすごいスピードで流し込まれたのでわかったようなつもりで大してわかってないんだけど、すごいので忘れない。きっと必要なときに思い出せると思う。すごすぎてこんな顔になった。

Language Serverと喋ろう

https://x.com/pizzacat83b

https://speakerdeck.com/pizzacat83/language-server-todie-rou-tskaigi-2025

Language Serverとは何か、意義、使い方、応用、AIなど幅広く理解できた。なぜLanguage Serverを使うのかという点も発表者のバックグラウンドから納得のいく説明がなされて、TSだけ書いていてはわかりにくい一段階高い視点が得られて有意義だった。

AI Coding Agent Enablement in TypeScript

https://x.com/yukukotani

https://speakerdeck.com/yukukotani/ai-coding-agents-enablement-in-typescript

AIコーディングエージェントを速く正確に動かすため、我々は何をすればよいのか。意味があることとないことが最新の研究成果の引用とともに紹介され、基本的な結論はそりゃそうだよねという感じでありつつも、そこに至る筋道が明確で非常に参考になった。「入力方法はどうでもいい(中略)大事なのは入力に値する情報の整備」は名言。開発を効率化するという包括的な視点の中でツールチェインに対しても考察されており良い。

fast-checkとneverthrowのPBT+Result型で堅牢なビジネスロジックを実現する

https://2025.tskaigi.org/talks/kueda

TSにはthrow型が無いのが辛い、という積年の問題について、現状を分析した上でneverthrowのようなライブラリをどのような形で、どこになら導入できるのかというのが経験に基づいて丁寧に考察されており、同じ問題に苦労している人間として地に足がついた報告が参考になった。さらにPBTという独特な手法についても知ることができた。

Rust製JavaScript/TypeScript Linterにおけるプラグイン実装の裏側

https://x.com/unvalley_

https://speakerdeck.com/unvalley/typescript-linters

Rust製LinterはESLintをパフォーマンスで上回るものの、ESLintが支持される理由であるプラグインシステムの再現にはRustがRustであるがゆえの困難がある。針の穴を通すような技術的な挑戦が僕らが日常的に使うツールチェーンを支えていることが実感される感動的な発表だった。yukukotani氏の発表でLinterの速度の重要性を再認識したところでこの発表が出るというのもアツかった。

その他

そこそこ知り合いがいたのと、スポンサーブースが普通に盛り上がっていて楽しい。エンジニアは問題を解きたがりなのでわかるかな?的な挑戦問題の掲示が多かった。お弁当おいしい。30分単位の発表は長くはないけど連打されるとそこそこ疲れる。集中してるし。自分的安牌みたいな発表を聞きがちだけど裏でトンデモアイデアみたいな発表もあったりして分身したい。数年前なら全然わかんねえよって話題ばっかりだったと思うけど、今はどの話題もそこそこ意義やポイントが理解できるので成長したなという実感がある。楽しいね。

20250521 肉に謎の硬いものが入ってた

項目 内容 得点 換算点
睡眠時間 7時間34分 100 13.0/13.0
起床 8:27 78 6.2/8.0
散歩 実施・ゴミ拾いなし 100 5.0/5.0
朝食の栄養カバレッジ 3色カバー 100 5.0/5.0
体操 ノー 0 0.0/5.0
労働 passion: 75点, discipline: 75点 75 18.0/24.0
ジム 休養日 100 12.0/12.0
勉強会 参加 100 12.0/12.0
個人開発 ノー 0 0.0/7.0
あすけん - 61 5.5/9.0
総合 1日の総合評価 - 77

よく眠れた。が、8時前後に散歩したいので起床時間をもっと前にしたい。ということで入眠を早める努力をしてみる。睡眠力で、世界を制覇する。

昼食は松屋のいつものセット。自動呼び出しシステムが止まってたし肉に謎の硬いものが入ってたし、割と悲しい寄りの食事だった。

Gemini2.5-flashの新バージョンが出たそうですね。前の2.5-flashはなかなかやるなという感想で、今回の2.5-flashもなかなかやるなと思った(違いがあまりわからない)。安くて強いモデルが出てくるのはいいことだ。最近は仕事でも上手くAIにやらせることを意識しているが、残念ながらかなりシンプル目のタスクでもAIがまともにやれたものは1つもない。長々指導していても途中で集中が切れるのか俺もAIも理解できないぐちゃぐちゃの変更を入れ始めて矢面海!!と絶叫することになる。既存プロダクトにAIが触れるような仕組みを用意してやるのは結構大変そうで、新規プロダクトの初速の方が強そうだ。