はじめに
D言語で自作のデータ構造の要素を順番に処理したい場合などに
foreach
文に対応させたい場合がありますが、そもそもforeach
が何をしていて
対応させるにはどのような方法があるのかよく知らなかったのでまとめてみました。
foreach
は「オブジェクトの種類に応じて、様々な形に展開される、繰り返し処理の糖衣構文」と理解できると、色々とすっきりしました。
https://dlang.org/spec/statement.html#ForeachStatement
auto arr = [1, 2, 3];
// 例えば、配列の場合のforeach文は
// 以下のようなfor文に展開される(イメージ)
//foreach (elem; arr) {
for (size_t __i; __i < arr.length; ++__i) {
auto elem = arr[__i];
// ...
}
isIterable
まずは既にforeach
に対応しているものを見ていきます。
foreach
でループ可能かどうかは、std.traits
にあるisIterable
テンプレートで判定できます。
ドキュメントを確認すると
- 配列、連想配列
- レンジ
-
opApply
を実装した構造体/クラス
などが例として挙げられていました。
(これ以外にも沢山あります)
alias this
元々foreach
に対応している配列や連想配列を利用することで
お手軽にforeach
に対応させることができそうです。
D言語でサブタイピングを行うには、alias this
を使います。
struct Stack {
int[] arr;
// as stack
void push(int n) { arr ~= n; }
void pop() { if (arr.length) arr.length--; }
int top() @property { return arr[$ - 1]; }
// as array
alias arr this; // 構造体にないメンバの呼び出しは、arrに転送される
}
void main() {
import std.stdio;
auto stack = Stack([1, 2, 3]);
stack.push(4);
foreach (i, e; stack) {
writefln!"stack[%s]: %s"(i, e);
}
stack.pop;
foreach (i, e; stack) {
writefln!"stack[%s]: %s"(i, e);
}
writeln("stack_top: ", stack.top);
}
stack[0]: 1
stack[1]: 2
stack[2]: 3
stack[3]: 4
stack[0]: 1
stack[1]: 2
stack[2]: 3
stack_top: 3
この方法はとても手軽でよいのですが、内部の配列が公開されているため、適切に制御/実装してあげる必要があります。
ちゃんとやろうとすると、これが意外と面倒です。
お手軽とはなんだったのか。
struct Stack {
// 省略
@disable { // 手抜き
void opAssign(int[]); // 直接代入の禁止
int[] opBinary(string op)(int); // 要素の連結禁止
int[] opBinary(string op)(int[]); // 配列の連結禁止
int[] opOpAssign(string op)(int); // 要素の連結&代入禁止
int[] opOpAssign(string op)(int[]); // 配列の連結&代入禁止
}
// as array
alias arr this;
}
元となる型に少しだけ手を加えたいときだけ使用するのがよさそう。
入力レンジ
現状、公式に推奨されている方法で、標準ライブラリの多くがこの方式に対応しています。
レンジの一種である入力レンジの要件を満たせば、
foreach
に限らず、std.algorithm
にあるmap
やeach
などにも対応できるため、汎用性が高いです。
struct Fib(int limit)
if (0 < limit && limit <= 10) {
private:
int index;
int calc(int n) { return n < 2 ? n : calc(n - 1) + calc(n - 2); }
public:
// 入力レンジの要件
int front() @property { return calc(index); }
bool empty() @property { return index >= limit; }
void popFront() { ++index; }
}
void main() {
import std.stdio, std.range, std.algorithm;
static assert(isInputRange!(Fib!1u));
auto fib5 = Fib!5u();
foreach (e; fib5) {
e.writeln;
}
fib5.each!writeln;
}
0
1
1
2
3
0
1
1
2
3
レンジの自作あるあるとしては、「何も考えずに実装すると
foreach
でレンジが消費されてしまう」というのがあります。
foreach
文がfor
文に展開される際に、for
文の初期化部分で、レンジの変数を一旦コピーしていて
auto rng = MyRange();
// 例えば、レンジの場合のforeach文は
// 以下のようなfor文に展開される(イメージ)
//foreach (elem; rng) {
for (auto __r = rng; !__r.empty; __r.popFront) {
auto elem = __r.front;
// ...
}
以降はコピー側の変数からの操作になるのですが、この時に元の変数に影響しないように実装(あるいは使用)する必要があります。
// 正しい例
struct MyRange {
int[] arr;
int front() @property { return arr[0]; }
bool empty() @property { return !arr.length; }
void popFront() { arr = arr[1 .. $]; }
}
auto rng = MyRange([1, 2, 3]);
foreach (elem; rng) {
//for (auto __r = rng; !__r.empty; __r.popFront) {
// 構造体は値セマンティクスなので、代入はビット単位のコピー
// rngのarrと__rのarrは同じインスタンスを参照する
// 別々のスライスなので、popFrontが__rのarrを更新しても
// rngのarr自体には影響がない
}
assert(rng.empty == false);
// 間違った例
class MyRange {
int[] arr;
this(int[] arr) { this.arr = arr; }
int front() @property { return arr[0]; }
bool empty() @property { return !arr.length; }
void popFront() { arr = arr[1 .. $]; }
}
auto rng = new MyRange([1, 2, 3]);
foreach (elem; rng) {
//for (auto __r = rng; !__r.empty; __r.popFront) {
// クラスは参照セマンティクスなので、代入はインスタンスの共有
// popFrontが__rのarrを更新すると、rngのarrも更新される
}
assert(rng.empty == true);
opApply
レンジに対応する必要がない場合や、対応することが(実装が複雑になったり、パフォーマンスが出ないなどの理由で)難しい場合にopApply
を使う方法があります。
この方法が以前はforeach
対応で一般的だったようですが、標準ライブラリの大部分がレンジに置き換わった現在はあまり見かけなくなりました。
opApply
は特殊なデリゲートを引数に取るメンバ関数です。
http://ddili.org/ders/d.en/foreach_opapply.html
構造体/クラスにopApply
というメンバ関数が存在していると、仮にレンジの要件を満たしていてもforeach
ではこちらが優先して使用されます。
struct Apply {
// ・・・
int opApply(int delegate(ループ変数) ループ本体) {
// データ構造を順番に辿りながら、要素ごとに
// ループ本体(ループ変数)を呼び出していく処理を記述
// ループ本体の戻り値が0なら続行、0以外なら終了
}
}
auto apply = Apply();
// 例えば、opApplyのforeach文は
// 以下のようなopApplyメンバ関数の呼び出しに展開される(イメージ)
//foreach (ループ変数; apply) {
// // ループ本体
//}
apply.opApply(
delegate int(ループ変数) {
// ループ本体
}
);
このデリゲートの本体はforeach
のループ本体の処理として、
デリゲートの引数はforeach
のループ変数として、それぞれ展開されます。
struct LinkedList {
private:
class Node {
this(int x) { payload = x; }
Node next;
int payload;
alias payload this;
}
Node head;
Node tail;
size_t size;
public:
this(int[] arr) {
foreach (e; arr) {
Node node = new Node(e);
if (size++ == 0) {
head = node;
} else {
tail.next = node;
}
tail = node;
}
}
int opApply(int delegate(ref Node node) body) {
int result = 0;
for (auto cur = head; cur; cur = cur.next) {
result = body(cur);
if (result != 0)
break;
}
return result;
}
}
void main() {
import std.stdio;
auto list = LinkedList([1, 2, 3]);
foreach (e; list) {
e.writeln;
}
}
1
2
3
基本はレンジ対応で、必要に応じてopApply
も実装する、というのがいいのかなと思いました。