Help us understand the problem. What is going on with this article?

D 2.067.0 の新機能(イテレーション篇)

More than 5 years have passed since last update.

先日 D 2.067.0 がリリースされたものの、現時点で change log が残念なので、イテレーション関連の機能のうち個人的に気になるものをちょっとだけ紹介してみます。
他の新機能については誰かが紹介してくれるはず。

導入

今リリースにおける新機能のうち、次に挙げるイテレーション関係のものを紹介します。

補足ですが、今リリースから std.algorithmstd.range といった巨大なモジュールがパッケージ化されましたが、子モジュールだけ import することも、今までどおり親モジュールの名前で一括 import することも可能です。

何をどうするの

レンジを UFCS でチェインしたワンライナーをよしとする宗派を信仰している関係で、サンプルコードが妙です。次のページが教義をよく説明していると思われますので、目を通しておくとよいでしょう。

上記にある例を参考に、以後の比較で使う D 2.066.1 までのコードを書いてみると、次のようになります。

example.d
void main(){
    import std.stdio, std.algorithm, std.range;

    5.iota.map!(a => (a.writeln, 0)).array;  // <---
}

期待される出力は次のとおりです。

output
0
1
2
3
4

余計な括弧は除去していますが、だいぶ無駄があります。これを書き換えるがために使えそうな新機能を選んだというだけです。

例示

std.algorithm.iteration.each

foreach の式バージョンみたいな関数です。最初の例を微改造すると次のようになります。

5.iota.map!(a => (a.writeln, 0)).each;

arrayeach になっただけですが、無駄に配列を作らずにループを回す感覚で副作用を起こせるので有用です。戻り値がないことに注意してください。
また、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 にインデックス付きで渡したいときに特に有用です。lockstepzip で頑張っていた処理が楽に書けるようになるかもしれません。
今回の例で無理矢理使うとすれば次のようになります。

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 やネストしたループの入口、出口のイメージのほうがわかりやすいかもしれません。
いずれにしろ、出力結果は次のようになるでしょう。

output
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 のリリースプロセス改革が始まりつつあり、その先陣を切るリリースであるがゆえか、冒頭に書いたように表沙汰になっていない変更点はまだまだあるはずです。今回紹介できなかった分は誰かに任せます。

e10s
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away