ほげほげー

C#メインにプログラミング周りから日常のあれこれとかを不定期に書いていきます。

JavaScript における yield の話とかアロー関数の話とか

JavaScript で業務用のブックマークレットでも書くかと思って書いてたら何か自分が知ってる JavaScript からだいぶ変わってたのでメモがてら。

最近の JavaScript について

もはや JavaScript を勉強したのは何年前だって状態なので、間の機能を補完しようかと思ったのですが、何が増えているのかさっぱりわからないのでとりあえず yieldラムダ式 アロー関数 の話でも。async/await もいいんですが、まぁこれは気が向いたら。

yield について

yield は、委譲するとか譲るとかの意味を持った英単語で、実際 yield の使用時には値を実処理側に委譲するような動きをします。

記述方法は以下の形式です。

function* sample() {
  yield "one";
  yield "two";
  yield "three";
}

function の後にアスタリスクを付けることで yield キーワードが使えるようになります。

この関数の戻り値は Generator 型となり、Generator.prototype.next() 関数や for of*1 を使って要素を一つずつ処理することができます。

let values = sample();
console.log(values.next()); // {value: "one", done: false}
console.log(values.next()); // {value: "two", done: false}
console.log(values.next()); // {value: "three", done: false}
console.log(values.next()); // {value: undefined, done: true}

ループで回して値を取得する場合は

let values = sample();
while ((let next = values.next()).done === false) {
  console.log(next.value);
}

または

let values = sample();
for (const value of values) {
  console.log(value);
}

で利用できます。基本は後者ですかね。

これができると何が嬉しいかというと、列挙処理において遅延評価がしやすくなる点と、同じことをしようとした場合に Iterator パターンを一から実装しないといけなかったものが簡潔に書けるようになった点にあります。

C# でも yield return でほぼ同様のことができる実現でき、それについてはこちらに諸々まとめてあります。

Generator 型について

さて、C# を慣れ親しんだ身としては yield で返された値は LINQ っぽく扱いたくなるもので、これは Generatorprototype を拡張してやればできるはずです。

とりあえず拡張して U Select<T, U>(this IEnumerable<T, U> source, Func<T, U> selector) 的なものを足してみましょう。

Generator.prototype.select = function*(seletor){for (item of this) { yield selector(item); }};

すると、Generator が見つからず、エラーになってしまいます。

Generator は直接インスタンスを生成することが許可されていないため、触れる場所にはありません。

では yield で返ってきた Generatorprototype を使ってみましょう。

const generator = (function*(){})();
generator.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};

できたっぽいので使ってみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
for (const item of sample.select(i => i * 10)) {
  console.log(item);
}

実行すると以下のエラーが発生します。

VM2202:7 Uncaught TypeError: s.select is not a function or its return value is not iterable

何故でしょうか?

C# で yield return に触れていると何となく原因はこれなのかな?
というものが頭に浮かびます。

yield は結局のところシンタックスシュガーで、Generator は実行エンジン側で動的に生成された型になっているのです。

つまり Generator を生成する関数毎に異なる型が返却されていることになります。

そこでデバッグコンソールからオブジェクトのプロトタイプチェーンを眺めてみると

f:id:tyhe:20210109020603p:plain

どうやら Generator の上にさらに Generator がいました。

こいつがどうやら大元っぽいのでこいつを拡張してみましょう。

const generator = (function*(){})();
generator.__proto__.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};

で、これを使ってみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
for (const item of sample.select(i => i * 10)) {
  console.log(item);
}

動きました。

これで Generator の拡張ができるようになりました。

any や all も欲しいのでこちらも実装してみましょう。

今回は Generator を返す必要がないので折角なのでアロー関数で書いてみましょう。

generator.__proto__.__proto__.any = (predicator = (_ => true)) => {
  for (const item of this) {
    if (predicator(item)) {
      return true;
    }
    return false;
  }
}

さて実行してみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
if (sample.any()) {
  console.log("success"):
}

実行すると以下のエラーが発生します。

VM1280:2 Uncaught TypeError: this is not iterable
    at Generator.generator.__proto__.__proto__.any

this が iterable じゃないと言われています。なるほど?

試しに function で再実装してみましょう。

generator.__proto__.__proto__.any = function(predicator = (_ => true)) {
  for (const item of this) {
    if (predicator(item)) {
      return true;
    }
    return false;
  }
}

そして実行。

if (sample.any()) {
  console.log("success"):
}

見事動きました。何故でしょう?

MDN を見ると以下のように記述されています。

this, arguments, super, new.target を束縛しません。

つまり this は記述されている環境に依存した値のまま変わらないということになります。

アロー関数は関数の引数に使用するなどに止めるのが良さそうですね。

*1:iterable protocol を満たす場合に for of を使うことができる