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
っぽく扱いたくなるもので、これは Generator
の prototype
を拡張してやればできるはずです。
とりあえず拡張して 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
で返ってきた Generator
の prototype
を使ってみましょう。
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
を生成する関数毎に異なる型が返却されていることになります。
そこでデバッグコンソールからオブジェクトのプロトタイプチェーンを眺めてみると
どうやら 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 を使うことができる