白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

yieldのおさらい (C#/TypeScript)

こんばんは。
天気がすぐ変わるので、いつも傘を持っている気がします。
雨傘って風の方向に向ければ雨も風も防げますが、日傘は風の方向と日の方向が連動しないのでひっくり返しがちです。

今回はyieldについて見ていこうと思います。
(いつも綴りが「yield」か「yeild」かわからなくなります)

そもそもyieldってどういう意味?

そこまで頻繁に使う機能でもないし、単語としてもあまり見かけないのでふんわりとした感覚でしか認識していなかったのですが、辞書を引いてみると「明け渡す」とか「譲る」とかそんなニュアンスのようです。
「中断する」とかそういう意味かと思っていました。そういえば債権の用語でイールドカーブってありますが、これは「利回り」かな。

プログラム言語においては一時停止と再開をコントロールする機能で、しばしばジェネレータに利用されます。
(あくまでyieldを使ってジェネレータを作れるだけで、yield=ジェネレータというわけではないです)
大きなデータをメモリ上にすべて読み込まず、必要な分だけ1つずつ処理することができます。

C#でのyield

C#では「yield return」という形で関数の呼び出し元に次の値を返します。
戻り値の型はIEnumerableでもIEnumeratorでもどちらでも大丈夫です。(ジェネリック版でもそうじゃなくてもOK)

foreach (var item in Get1())
{
    Debug.WriteLine(item);
}

var inumerator = Get2();
while (inumerator.MoveNext())
{
    var item = inumerator.Current;
    Debug.WriteLine(item);
}

IEnumerable<string> Get1()
{
    yield return "1";
    yield return "2";
    yield return "3";
}

IEnumerator<string> Get2()
{
    yield return "4";
    yield return "5";
    yield return "6";
}

イメージとしてはあくまで一時停止とか中断とかそういう感じなので、普通に関数を実行して「yield return」にたどり着いたら値を返し、次の値を求められたら続きを再開するような挙動です。
なので最初の「yield return」の前に処理があれば最初に実行され、最後の「yield return」のあとに処理があれば最後に実行されます。
finallyもちゃんと実行されますが、catchしようとすると「catchを含むtryブロックの本体で値を生成することはできません」とコンパイルエラーになります。

foreach (var item in Get3())
{
    Debug.WriteLine(item);
}

IEnumerable<string> Get3()
{
    try
    {
        Debug.WriteLine("get3 start");
        yield return "7";
        Debug.WriteLine("hoge");
        yield return "8";
        Debug.WriteLine("fuga");
        yield return "9";
        Debug.WriteLine("get3 end");
    }
    finally
    {
        Debug.WriteLine("finally");
    }
}
// get3 start
// 7
// hoge
// 8
// fuga
// 9
// get3 end
// finally

「yield break」を使うと明示的に値の生成を中断することができます。ただ、この場合は普通の「break」で書いても結果は一緒ですね。あんまり使わないような…。

foreach (var item in Get4())
{
    Debug.WriteLine(item);
}

IEnumerable<string> Get4()
{
    for (var i = 0; i < 10; i++)
    {
        if (i % 2 == 0)
        {
            yield return i.ToString();
        }
        else if (i % 7 == 0)
        {
            yield break;
        }
    }
}
// 0
// 2
// 4
// 6

IEnumerableをyieldで返すことはできないので、ジェネレータ関数の中でさらにジェネレータ関数を呼んだ場合は自分でループを回す感じになります。

IEnumerable<string> Get5()
{
    // こうは書けない
    // yield return Get6();

    // こう書く
    foreach (var item in Get6())
    {
        yield return item;
    }
}

IEnumerable<string> Get6()
{
    yield return "a";
    yield return "b";
}

TypeScriptでのyield

基本的な考え方はC#と変わりませんが、TypeScriptではyieldを使う際には「function*」という表記で明示的に宣言します。
そして「yield return」ではなく「yield」です。戻り値の型はIterableIteratorという不思議な名前のインタフェースです。
でもよく考えるとIterableでもあり、IteratorでもあるっていうのはC#でIEnumerableでもIEnumeratorでもOKというのと一緒ですね。

for (let item of get1()) {
    console.log(item);
}

function* get1() : IterableIterator<number> {
    yield 1;
    yield 2;
    yield 3;
}

finallyが使えるのは一緒ですが、普通にcatchもできます。
「yield break」みたいな文法はなさそうですね。

function* get2() : IterableIterator<number> {
    try
    {
        console.log("get2 start");
        yield 4;
        console.log("hoge");
        yield 5;
        throw new Error("error");
        yield 6;
        console.log("get2 end");
    }
    catch(e)
    {
        console.log("catch");
    }
    finally
    {
        console.log("finally");
    }
}
// get2 start
// 4
// hoge
// 5
// catch
// finally

ジェネレータ関数の中でさらにジェネレータ関数を呼ぶ場合には「yield*」を使うとそのまま返せます。便利。

function* get3() : IterableIterator<string> {
    yield* get4();
}
function* get4() : IterableIterator<string> {
    yield "a";
    yield "b";
}
// a
// b

全体的にC#より使いやすい感じがします。

で、いつ使うの

というわけで使い方はわかりましたが、いつ使うのかという話です。
最初に書いた通り、オンメモリに大きなデータを乗せたくないときに便利ですが、中間処理はC#ならLINQ、TypeScriptなら反復処理メソッドを使えば自然と遅延評価になります。

となると最初にコレクションや配列を作るとき…となるのでジェネレータ(生成関数)って話になるわけですね。
無限配列のように使う側で良い感じに途中で打ち切るという場合にも、生成側は「yield」を使うと便利そうです。

例えばこんな感じに組み合わせ(順列)を作るとかも良さそうです。

for (let item of generator("abc", 3, "")) {
    console.log(item);
}

function* generator(characters: string, length: number, s: string) : IterableIterator<string> {
    if (s.length === length) {
        yield s;
    } else {
        for (let i = 0; i < characters.length; i++) {
            yield* generator(characters, length, s + characters[i]);
        }
    }
}
// aaa
// aab
// aac
...
// cca
// ccb
// ccc

使い方を間違えると変なタイミングで変な処理が呼ばれること(例えばコネクションが閉じてるのにDBから続きのデータを読み出そうとするとか)もありますが、便利な機能なのでうまく付き合っていきたいですね。