先日 D 2.067.0 がリリースされたものの、現時点で change log が残念なので、イテレーション関連の機能のうち個人的に気になるものをちょっとだけ紹介してみます。
他の新機能については誰かが紹介してくれるはず。
導入
今リリースにおける新機能のうち、次に挙げるイテレーション関係のものを紹介します。
補足ですが、今リリースから std.algorithm
や std.range
といった巨大なモジュールがパッケージ化されましたが、子モジュールだけ import
することも、今までどおり親モジュールの名前で一括 import
することも可能です。
何をどうするの
レンジを UFCS でチェインしたワンライナーをよしとする宗派を信仰している関係で、サンプルコードが妙です。次のページが教義をよく説明していると思われますので、目を通しておくとよいでしょう。
上記にある例を参考に、以後の比較で使う D 2.066.1 までのコードを書いてみると、次のようになります。
void main(){
import std.stdio, std.algorithm, std.range;
5.iota.map!(a => (a.writeln, 0)).array; // <---
}
期待される出力は次のとおりです。
0
1
2
3
4
余計な括弧は除去していますが、だいぶ無駄があります。これを書き換えるがために使えそうな新機能を選んだというだけです。
例示
std.algorithm.iteration.each
foreach
の式バージョンみたいな関数です。最初の例を微改造すると次のようになります。
5.iota.map!(a => (a.writeln, 0)).each;
array
が each
になっただけですが、無駄に配列を作らずにループを回す感覚で副作用を起こせるので有用です。戻り値がないことに注意してください。
また、each
にはテンプレート引数を与えることができます。
5.iota.each!writeln;
map
を消せました。map
に渡す関数は戻り値がなければならないこと、ラムダ構文を使いたいがゆえにカンマ演算子を使っていますが、カンマ演算子を廃止しようという議論があって(うやむやになったけど)震えていたところ、無事に導入された優れものです。
また、テンプレート引数にはラムダを渡すこともできます。
5.iota.each!(a => a.writeln);
この each
は以後の例で多用します。
std.concurrency.Generator
ジェネレータです。クラスです。ループを入力レンジに変換できるので有用です。引数を取らずに、内部で yield
を呼び出すようにした関数をコンストラクタに渡します。
new Generator!int({foreach(i; 0..5) i.yield;}).each!writeln;
nextPermutation
のレンジ化にも有用です。
無駄に Generator
を使いたければ次のように書けます。
new Generator!int(() => 5.iota.each!yield).each!writeln;
注意点としては、所属するモジュールの性質上コンパイル時処理できません。
enum a = new Generator!int(() => 5.iota.each!yield).array; // bad
そもそも、モジュール的に、今回の例のような用途では役不足(誤用じゃない)かもしれません。本来の用途など、詳しいことは repeatedly の人が説明してくれるはずです。
std.range.enumerate
入力レンジをインデックス付きのレンジにする関数です。foreach
にインデックス付きで渡したいときに特に有用です。lockstep
や zip
で頑張っていた処理が楽に書けるようになるかもしれません。
今回の例で無理矢理使うとすれば次のようになります。
false.repeat(5).enumerate.each!(a => a[0].writeln);
front
するとインデックスと要素のタプルが返されますが、要素はどうでもよく、インデックスを出力したいという観点で false.repeat(5)
という無駄な書き方をしてみました。
また、enumerate
には開始インデックスを引数として渡すこともできます。
std.range.tee
レンジ処理のチェインの間に挟んで、その値を覗く関数です。T 字形の何かです。チェインの前後で値を変えたくない場合に有用です。リファレンスにサンプルコードがないので、ソースファイルの unittest
を読んでください。闇を感じます。
普通に使う分には簡単です。
5.iota.tee!writeln.each;
pipeOnPop
というテンプレート引数が闇です。リファレンスの補足がてら、説明します。
3.iota.tee!(a => writeln("a ", a)).map!"a+10".tee!(a => writeln("b ", a)).each!(a => writeln("c ", a));
writeln;
3.iota.tee!(a => writeln("a ", a), No.pipeOnPop).map!"a+10".tee!(a => writeln("b ", a)).each!(a => writeln("c ", a));
writeln;
3.iota.tee!(a => writeln("a ", a), No.pipeOnPop).map!"a+10".tee!(a => writeln("b ", a), No.pipeOnPop).each!(a => writeln("c ", a));
writeln;
3.iota.tee!(a => writeln("a ", a)).map!"a+10".tee!(a => writeln("b ", a), No.pipeOnPop).each!(a => writeln("c ", a));
いつ覗くかというパラメータです。このチェインを、iota
から右に進み、each
に達してから再び iota
に向けて左に戻ると見れば、No.pipeOnPop
では行きで覗き、Yes.pipeOnPop
では帰りで、ということでしょう。個人的には、DOM のイベントのキャプチャフェイズ、バブリングフェイズのイメージですが、普通に、スタックの push/pop やネストしたループの入口、出口のイメージのほうがわかりやすいかもしれません。
いずれにしろ、出力結果は次のようになるでしょう。
c 10
b 10
a 0
c 11
b 11
a 1
c 12
b 12
a 2
a 0
c 10
b 10
a 1
c 11
b 11
a 2
c 12
b 12
a 0
b 10
c 10
a 1
b 11
c 11
a 2
b 12
c 12
b 10
c 10
a 0
b 11
c 11
a 1
b 12
c 12
a 2
呼び出しのタイミングがそれぞれ異なることがわかります。
締め
DMD のリリースプロセス改革が始まりつつあり、その先陣を切るリリースであるがゆえか、冒頭に書いたように表沙汰になっていない変更点はまだまだあるはずです。今回紹介できなかった分は誰かに任せます。