LoginSignup
2
1

More than 5 years have passed since last update.

foreachとその対応まとめ

Posted at

はじめに

D言語で自作のデータ構造の要素を順番に処理したい場合などに
foreach文に対応させたい場合がありますが、そもそもforeachが何をしていて
対応させるにはどのような方法があるのかよく知らなかったのでまとめてみました。

foreachは「オブジェクトの種類に応じて、様々な形に展開される、繰り返し処理の糖衣構文」と理解できると、色々とすっきりしました。
https://dlang.org/spec/statement.html#ForeachStatement

D
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を使います。

D
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);
}
Output
stack[0]: 1
stack[1]: 2
stack[2]: 3
stack[3]: 4
stack[0]: 1
stack[1]: 2
stack[2]: 3
stack_top: 3

この方法はとても手軽でよいのですが、内部の配列が公開されているため、適切に制御/実装してあげる必要があります。
ちゃんとやろうとすると、これが意外と面倒です。
お手軽とはなんだったのか。

D
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にあるmapeachなどにも対応できるため、汎用性が高いです。

D
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;
}
Output
0
1
1
2
3
0
1
1
2
3

レンジの自作あるあるとしては、「何も考えずに実装すると
foreachでレンジが消費されてしまう」というのがあります。

foreach文がfor文に展開される際に、for文の初期化部分で、レンジの変数を一旦コピーしていて

D
auto rng = MyRange();
// 例えば、レンジの場合のforeach文は
// 以下のようなfor文に展開される(イメージ)
//foreach (elem; rng) {
for (auto __r = rng; !__r.empty; __r.popFront) {
    auto elem = __r.front;
    // ...
}

以降はコピー側の変数からの操作になるのですが、この時に元の変数に影響しないように実装(あるいは使用)する必要があります。

D
// 正しい例
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);
D
// 間違った例
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ではこちらが優先して使用されます。

D
struct Apply {
    // ・・・
    int opApply(int delegate(ループ変数) ループ本体) {
        // データ構造を順番に辿りながら、要素ごとに
        // ループ本体(ループ変数)を呼び出していく処理を記述
        // ループ本体の戻り値が0なら続行、0以外なら終了
    }
}
auto apply = Apply();
// 例えば、opApplyのforeach文は
// 以下のようなopApplyメンバ関数の呼び出しに展開される(イメージ)
//foreach (ループ変数; apply) {
//    // ループ本体
//}
apply.opApply(
    delegate int(ループ変数) {
        // ループ本体
    }
);

このデリゲートの本体はforeachのループ本体の処理として、
デリゲートの引数はforeachのループ変数として、それぞれ展開されます。

D
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;
    }
}
Output
1
2
3

基本はレンジ対応で、必要に応じてopApplyも実装する、というのがいいのかなと思いました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1