ユニバーサル関数呼び出し構文(UFCS)

UFCSは、コンパイラによって自動的に適用される機能だ。これにより、通常の関数でもメンバー関数構文を使用できるようになる。これは、2つの式を比較することで簡単に説明できる。

variable.foo(arguments)
D

コンパイラが上記のような式を見つけた場合、variableで指定された引数で呼び出すことができるfooという名前のメンバー関数が存在しない場合、コンパイラは次の式もコンパイルしようと試みる。

foo(variable, arguments)
D

この新しい式が実際にコンパイルできる場合、コンパイラは単にその式を受け入れる。その結果、foo()は明らかに通常の関数であるにもかかわらず、メンバー関数構文で使用できると認識される。

注釈:UFCSは、モジュールスコープで定義された関数のみを考慮する。例えば、ネストされた関数はUFCS構文では呼び出せない。

型と密接に関連する関数は、その型のメンバー関数として定義されることはご存じだろう。これは、型のメンバー関数(およびその型のモジュール)だけがその型のprivateメンバーにアクセスできるため、カプセル化にとって特に重要だ。

燃料の量を管理するCarクラスを考えてみよう。

class Car {
    enum economy = 12.5;          // キロメートルあたりリットル(平均)
    private double fuelAmount;    // リットル

    this(double fuelAmount) {
        this.fuelAmount = fuelAmount;
    }

    double fuel() const {
        return fuelAmount;
    }

    // ...
}

メンバー関数は非常に便利で、時には必要不可欠だが、型に対して動作するすべての関数がメンバー関数であるべきではない。型に対する一部の操作は、特定のアプリケーションに固有であり、メンバー関数としては不適切だ。例えば、自動車が特定の距離を走行できるかどうかを判断する関数は、通常の関数として定義するほうが適切だろう。

bool canTravel(Car car, double distance) {
    return (car.fuel() * car.economy) >= distance;
}
D

これにより、型に関連する関数の呼び出しに不整合が生じる。2つの構文では、オブジェクトが異なる場所に表示される。

void main() {
    auto car = new Car(5);

    auto remainingFuel = car.fuel();  // メンバー関数の構文

    if (canTravel(car, 100)) {        // 通常の関数の構文
        // ...
    }
}
D

UFCSでは、通常の関数をメンバー関数構文で呼び出すことができるようにすることで、この不一致を解消している。

if (car.canTravel(100)) {  // 通常の関数、メンバー関数構文によって
                           // 呼び出される
    // ...
}
D

この機能は、リテラルを含む基本的な型でも使用できる。

int half(int value) {
    return value / 2;
}

void main() {
    assert(42.half() == 21);
}

次の章で見るように、関数に渡す引数がない場合、その関数は括弧なしで呼び出すことができる。この機能も使用すると、上記の式はさらに短くなる。次の3つの文はすべて同等だ。

result = half(value);
result = value.half();
result = value.half;
D

UFCSは、関数呼び出しが連鎖している場合に特に便利だ。intスライスを操作する一連の関数で、これを確認しよう。

// すべての要素を'divisor'で
// 割った結果を返す
int[] divide(int[] slice, int divisor) {
    int[] result;
    result.reserve(slice.length);

    foreach (value; slice) {
        result ~= value / divisor;
    }

    return result;
}

// すべての要素を'multiplier'で
// 乗算した結果を返す
int[] multiply(int[] slice, int multiplier) {
    int[] result;
    result.reserve(slice.length);

    foreach (value; slice) {
        result ~= value * multiplier;
    }

    return result;
}

// 奇数値を持つ要素をフィルタリングする
int[] evens(int[] slice) {
    int[] result;
    result.reserve(slice.length);

    foreach (value; slice) {
        if (!(value % 2)) {
            result ~= value;
        }
    }

    return result;
}

UFCSを利用せずに通常の構文で記述した場合、これらの関数を3回呼び出す式は、次のプログラムのように記述できる。

import std.stdio;

// ...

void main() {
    auto values = [ 1, 2, 3, 4, 5 ];
    writeln(evens(divide(multiply(values, 10), 3)));
}
D

値はまず10倍され、次に3で割られ、最後に偶数だけが使用される。

[6, 10, 16]

上記の式の問題点は、multiply10のペアは関連しており、divide3のペアも関連しているにもかかわらず、各ペアの一部が互いに離れて記述されてしまうことだ。UFCSを使用すると、この問題が解決され、実際の演算の順序を反映した、より自然な構文を使用することができる。

writeln(values.multiply(10).divide(3).evens);
D

一部のプログラマーは、writeln()のような呼び出しでもUFCSを活用している:

values.multiply(10).divide(3).evens.writeln;
D

余談だが、上記のプログラム全体は、map()filter()を使用することで、はるかにシンプルな形で記述することができた。

import std.stdio;
import std.algorithm;

void main() {
    auto values = [ 1, 2, 3, 4, 5 ];

    writeln(values
            .map!(a => a * 10)
            .map!(a => a / 3)
            .filter!(a => !(a % 2)));
}

上記のプログラムは、テンプレート範囲ラムダ関数を利用している。これらについては、後の章で説明する。