その他の関数

関数については、この本ではこれまでの章で次のように説明している。

この章では、関数のその他の機能について説明する。

戻り値の型属性

関数は、autorefinout、およびauto refとしてマークすることができる。これらの属性は、関数の戻り値の型に関するものである。

auto関数

auto関数の戻り値の型は指定する必要はない。

auto add(int first, double second) {
    double result = first + second;
    return result;
}
D

戻り値の型は、return式からコンパイラによって推測される。resultの型はdoubleなので、add()の戻り値の型はdoubleになる。

return式が複数ある場合、関数の戻り値の型はそれらの共通型になる。(共通型については、三項演算子 ?:の章で説明した。) 例えば、intdoubleの共通型はdoubleなので、次のauto関数の戻り値の型もdoubleになる。

auto func(int i) {
    if (i < 0) {
        return i;      // ここでは'int'を返す
    }

    return i * 1.5;    // ここでは'double'を返す
}

void main() {
    // この関数の戻り値の型は'double'だ
    auto result = func(42);
    static assert(is (typeof(result) == double));
}
D
functions_more.1
ref関数

通常、関数から返される式は呼び出し元のコンテキストにコピーされる。refは、その式を代わりに参照によって返すことを指定する。

例えば、次の関数は、2つのパラメータのうち大きい方を返す。

int greater(int first, int second) {
    return (first > second) ? first : second;
}
D

通常、この関数のパラメータと戻り値の両方がコピーされる。

import std.stdio;

void main() {
    int a = 1;
    int b = 2;
    int result = greater(a, b);
    result += 10;                // ← aもbも変化しない
    writefln("a: %s, b: %s, result: %s", a, b, result);
}
D
functions_more.2

greater()の戻り値はresultにコピーされるため、resultに追加しても、その変数だけに影響し、abも変更されない。

ab結果
1212

refパラメータはコピーされるのではなく、参照によって渡される。同じキーワードは、戻り値にも同じ効果をもたらす。

ref int greater(ref int first, ref int second) {
    return (first > second) ? first : second;
}
D

この場合、返される参照は引数のいずれかのエイリアスとなり、返された参照を変更すると、aまたはbのいずれかが変更される:

int a = 1;
int b = 2;
greater(a, b) += 10;         // ← aまたはbの変更
writefln("a: %s, b: %s", a, b);
D

返される参照は直接加算されることに注意。その結果、2つの引数のうち大きい方が変更される。

ab
112

ローカル参照にはポインタが必要だ。重要な点は、戻り値の型はrefと指定されているが、戻り値がローカル変数に代入されても、aおよびbは変更されないことだ。

int result = greater(a, b);
result += 10;                // ← resultのみ変更
D

greater()aまたはbへの参照を返すが、その参照はローカル変数resultにコピーされるため、abも変更されない:

ab結果
1212

resultaまたはbへの参照であるためには、ポインタとして定義する必要がある:

int * result = &greater(a, b);
*result += 10;
writefln("a: %s, b: %s, result: %s", a, b, *result);
D

この場合、resultaまたはbへの参照となり、それを通じた変更は実際の変数に影響を与える:

ab結果
11212

ローカル変数への参照を返すことはできない。 refの戻り値は、関数が呼び出される前にその存在を開始した引数の1つへのエイリアスである。つまり、aまたはbへの参照が返されるかどうかに関係なく、返された参照は、まだ存在している変数を指す。

逆に、関数を離れる時点で存在していない変数への参照を返すことはできない。

ref string parenthesized(string phrase) {
    string result = '(' ~ phrase ~ ')';
    return result;    // ← コンパイルエラー
} // ← resultのライフタイムはここで終了する
D

ローカル変数resultの寿命は、関数を離れると終了する。そのため、その変数への参照を返すことはできない。

エラー: ローカル変数resultへの参照のエスケープ
Undefined
auto ref関数

auto refは、上記のparenthesized()のような関数で役立つ。autoと同様に、auto ref関数の戻り値の型はコンパイラによって推測される。さらに、戻り値の式が参照である場合は、その変数はコピーされるのではなく、参照として返される。

parenthesized()戻り値の型がauto refであれば、コンパイルできる。

auto ref string parenthesized(string phrase) {
    string result = '(' ~ phrase ~ ')';
    return result;                  // ← コンパイルできる
}
D

関数の最初のreturn文によって、関数がコピーを返すか参照を返すかが決まる。

auto refは、テンプレートパラメータがコンテキストに応じて参照またはコピーになる可能性がある関数テンプレートでより有用だ。

inout関数

inoutキーワードは、関数のパラメータ型および戻り値の型に使用される。これは、constimmutable、およびmutableのテンプレートのように機能する。

前の関数を、string(つまりimmutable(char)[]) を引数に取り、stringを返す関数に書き直しよう。

string parenthesized(string phrase) {
    return '(' ~ phrase ~ ')';
}

// ...

    writeln(parenthesized("hello"));
D

予想通り、このコードはstring引数で動作する:

(こんにちは)

しかし、この関数はimmutable文字列でしか動作しないため、本来の可能性よりも有用性が低いと言える。

char[] m;    // 変更可能な要素がある
m ~= "hello";
writeln(parenthesized(m));    // ← コンパイルエラー
D
エラー: 関数deneme.parenthesized (string phrase)は、
引数の型(char[])を使用して呼び出すことはできない
Undefined

同じ制限は、const(char)[]文字列にも適用される。

この使いやすさの問題に対する1つの解決策は、constおよび変更可能な文字列に対して関数をオーバーロードすることだ。

char[] parenthesized(char[] phrase) {
    return '(' ~ phrase ~ ')';
}

const(char)[] parenthesized(const(char)[] phrase) {
    return '(' ~ phrase ~ ')';
}
D

この設計は、コードの重複が明らかなため、あまり理想的とは言えない。別の解決策としては、関数をテンプレートとして定義することだ。

T parenthesized(T)(T phrase) {
    return '(' ~ phrase ~ ')';
}
D

この方法は機能するが、今回は柔軟性が過剰でテンプレート制約が必要になる可能性があるため、好ましくないかもしれない。

inoutは、テンプレートの解決策とよく似ている。違いは、型全体ではなく、可変性属性だけがパラメータから推論される点だ。

inout(char)[] parenthesized(inout(char)[] phrase) {
    return '(' ~ phrase ~ ')';
}
D

inoutは、推測された可変性属性を戻り値の型に転送する。

char[]で関数が呼び出されると、inoutがまったく指定されていないかのようにコンパイルされる。一方、immutable(char)[]またはconst(char)[]で呼び出されると、inoutはそれぞれimmutableまたはconstを意味する。

次のコードは、返される式の型を出力して、これを示している。

char[] m;
writeln(typeof(parenthesized(m)).stringof);

const(char)[] c;
writeln(typeof(parenthesized(c)).stringof);

immutable(char)[] i;
writeln(typeof(parenthesized(i)).stringof);
D

出力:

char[]
const(char)[]
string
動作属性

purenothrow@nogcは、関数の動作に関するものである。

pure関数

関数の章で見たように、関数は戻り値と副作用を生成することができる。可能であれば、戻り値を副作用よりも優先すべきである。副作用のない関数は理解しやすく、その結果、プログラムの正確性や保守性が向上するからだ。

同様の概念として、関数の純度がある。Dでは、純度は他のほとんどのプログラミング言語とは異なって定義されている。Dでは、変更可能なグローバル状態やstatic状態にアクセスしない関数はpureである。(入力および出力ストリームは変更可能なグローバル状態と見なされるため、pure関数は入力または出力操作も実行できない。)

つまり、関数は、そのパラメータ、ローカル変数、および不変のグローバル状態にのみアクセスして、戻り値と副作用を生成する場合、pureであると言える。

Dのpure性の重要な側面は、pure関数はそのパラメータを変化させることができることだ。

さらに、プログラムのグローバル状態を変更する以下の操作は、pure関数では明示的に許可されている。

pureキーワードは、関数がこれらの条件に従って動作することを指定し、コンパイラはそれが確実に実行されるように保証する。

当然のことながら、pureでない関数は同じ保証を提供しないため、pure関数はpureでない関数を呼び出すことはできない。

次のプログラムは、pure関数で実行できる操作と実行できない操作の一部を示している。

import std.stdio;
import std.exception;

int mutableGlobal;
const int constGlobal;
immutable int immutableGlobal;

void impureFunction() {
}

int pureFunction(ref int i, int[] slice) pure {
    // 例外をスローできる:
    enforce(slice.length >= 1);

    // パラメータを変更できる:
    i = 42;
    slice[0] = 43;

    // 不変のグローバル状態にアクセスできる:
    i = constGlobal;
    i = immutableGlobal;

    // 新しい式を使用できる:
    auto p = new int;

    // 変更可能なグローバル状態にアクセスできない:
    i = mutableGlobal;    // ← コンパイルエラー

    // 入力および出力操作を実行できない:
    writeln(i);           // ← コンパイルエラー

    static int mutableStatic;

    // 変更可能な静的状態にアクセスできない:
    i = mutableStatic;    // ← コンパイルエラー

    // 純粋でない関数を呼び出せない:
    impureFunction();     // ← コンパイルエラー

    return 0;
}

void main() {
    int i;
    int[] slice = [ 1 ];
    pureFunction(i, slice);
}
D
functions_more.3

一部のpure関数は、そのパラメータを変更することは許可されているが、実際には変更しない。pure性の規則に従うと、このような関数の唯一観察可能な効果は、その戻り値だけになる。さらに、この関数は変更可能なグローバル状態にアクセスできないため、プログラムの実行中にその関数がいつ、何回呼び出されたかに関係なく、指定された引数のセットに対して戻り値は同じになる。この事実により、コンパイラとプログラマの両方に最適化の可能性が生まれる。例えば、特定の引数のセットに対して関数を 2 度呼び出す代わりに、1 度目の呼び出しの戻り値をキャッシュして、実際に再び関数を呼び出す代わりにそれを使用することができる。

テンプレートのインスタンス化のために生成される正確なコードは、実際のテンプレート引数によって異なるため、生成されるコードがpureであるかどうかは、引数にも依存する。そのため、テンプレートのpure性は、生成されたコードからコンパイラによって推測される。(pureキーワードは、プログラマが指定することはできる。)同様に、auto関数のpure性も推測される。

簡単な例として、Nが0の場合、次の関数テンプレートはpureでないため、pure関数からtempl!0()を呼び出すことはできない。

import std.stdio;

// Nが0の場合、このテンプレートはpureではない
void templ(size_t N)() {
    static if (N == 0) {
        // Nが0の場合に表示される:
        writeln("zero");
    }
}

void foo() pure {
    templ!0();    // ← コンパイルエラー
}

void main() {
    foo();
}
D
functions_more.4

コンパイラは、テンプレートの0のインスタンス化がpureでないことを推測し、pure関数foo()からの呼び出しを拒否する。

エラー: pure関数'deneme.foo'は、pureでない関数
'deneme.templ!0.templ'を呼び出すことはできない
Undefined

しかし、0以外の値に対するテンプレートのインスタンス化はpureであるため、そのような値に対してはプログラムをコンパイルすることができる。

void foo() pure {
    templ!1();    // ← コンパイルできる
}
D

前述のように、writeln()のような入力および出力関数は、グローバル状態にアクセスするため、pure関数では使用できない。デバッグ中に一時的にメッセージを表示する必要がある場合など、このような制限が厳しすぎる場合もある。そのため、debugとマークされたコードについては、pure性のルールが緩和されている。

import std.stdio;

debug size_t fooCounter;

void foo(int i) pure {
    debug ++fooCounter;

    if (i == 0) {
        debug writeln("i is zero");
        i = 42;
    }

    // ...
}

void main() {
    foreach (i; 0..100) {
        if ((i % 10) == 0) {
            foo(i);
        }
    }

    debug writefln("foo is called %s times", fooCounter);
}
D
functions_more.5

上記のpure関数は、グローバル変数を変更してメッセージを出力することにより、プログラムのグローバル状態を変更する。これらのpureでない操作にもかかわらず、これらの操作はdebugとマークされているため、コンパイルは可能だ。

注釈:これらの文は、‑debugコマンドラインスイッチを使用してプログラムがコンパイルされた場合にのみ、プログラムに含まれることを覚えておいてほしい。

メンバー関数も、pureとマークすることができる。サブクラスは、pureとして不純な関数をオーバーライドすることができるが、その逆は許可されていない。

interface Iface {
    void foo() pure;    // サブクラスはfooをpureとして定義しなければならない。

    void bar();         // サブクラスはbarをpureとして定義することができる。
}

class Class : Iface {
    void foo() pure {   // pureである必要がある
        // ...
    }

    void bar() pure {   // pureであるが、必須ではない
        // ...
    }
}
D

デリゲートおよび匿名関数もpureにすることができる。テンプレートと同様に、関数リテラル、デリゲートリテラル、またはauto関数がpureであるかどうかは、コンパイラによって推測される。

import std.stdio;

void foo(int delegate(double) pure dg) {
    int i = dg(1.5);
}

void main() {
    foo(a => 42);                // ← コンパイルする

    foo((a) {                    // ← コンパイルエラー
            writeln("hello");
            return 42;
        });
}
D
functions_more.6

foo() 上記では、そのパラメータがpureなデリゲートである必要がある。コンパイラは、ラムダ式a => 42がpureであると推測し、foo()の引数として許容する。ただし、他のデリゲートはpureではないため、foo()に渡すことはできない。

エラー: 関数deneme.foo (int delegate(double) pure dg)は、
引数の型(void)を使用して呼び出すことはできない
Undefined

pure関数の利点の1つは、その戻り値をimmutable変数の初期化に使用できることだ。以下のmakeNumbers()によって生成される配列は変更可能だが、その要素は、その関数の外部にあるコードでは変更できない。そのため、初期化は正常に動作する。

int[] makeNumbers() pure {
    int[] result;
    result ~= 42;
    return result;
}

void main() {
    immutable array = makeNumbers();
}
D
functions_more.7
nothrow関数

例外メカニズムは例外の章で説明した

関数では、特定のエラー条件下でスローされる例外の型を文書化しておくことが望ましいだろう。ただし、原則として、呼び出し側は、どの関数もどの例外もスローする可能性があることを想定しておく必要がある。

関数がまったく例外を発生させないことを知っておくことがより重要な場合もある。例えば、一部のアルゴリズムは、その手順の一部が例外によって中断されないという事実を利用することができる。

nothrow関数が例外を発生させないことを保証する。

int add(int lhs, int rhs) nothrow {
    // ...
}
D

注釈: Errorおよびその基底クラスThrowableをキャッチすることは推奨されないことを覚えておいてほしい。ここでいう"あらゆる例外"とは、"Exception階層で定義されているあらゆる例外"を意味する。nothrow関数は、プログラムの実行を継続できない回復不可能なエラー状態を表すError階層にある例外を発生させることはできる。

このような関数は、それ自体で例外をスローすることも、例外をスローする可能性のある関数を呼び出すこともできない。

int add(int lhs, int rhs) nothrow {
    writeln("adding");    // ← コンパイルエラー
    return lhs + rhs;
}
D

add()はスローしないという保証に違反しているため、コンパイラはコードを拒否する。

エラー: 関数'deneme.add'はnothrowではないのにスローする可能性がある
Undefined

これは、writelnnothrow関数ではない(また、そうなることもできない)ためである。

コンパイラは、関数が例外を発生させることは決してないことを推測できる。nothrowの以下の実装は、add()である。これは、try-catchブロックによって、例外が関数から逃れることができないことがコンパイラに明らかだからだ。

int add(int lhs, int rhs) nothrow {
    int result;

    try {
        writeln("adding");    // ← コンパイルできる
        result = lhs + rhs;

    } catch (Exception error) {   // すべての例外をキャッチする
        // ...
    }

    return result;
}
D

前述のように、nothrowには、Error階層にある例外は含まれない。例えば、[]を使用して配列の要素にアクセスすると、RangeErrorがスローされる可能性があるが、次の関数はnothrowとして定義することができる。

int foo(int[] arr, size_t i) nothrow {
    return 10 * arr[i];
}
D

純度と同様に、テンプレート、デリゲート、および匿名関数がnothrowであるかどうかは、コンパイラが自動的に推測する。

@nogc関数

Dはガベージコレクション言語だ。ほとんどのDプログラムのデータ構造やアルゴリズムは、ガベージコレクタ(GC)によって管理される動的メモリブロックを利用している。このようなメモリブロックは、ガベージコレクションと呼ばれるアルゴリズムによってGCによって再利用される。

よく使用されるD操作のいくつかは、GCも利用している。例えば、配列の要素は動的メモリブロック上に存在する。

// GCを間接的に利用する関数
int[] append(int[] slice) {
    slice ~= 42;
    return slice;
}
D

スライスに十分な容量がない場合、上記の~=演算子はGCから新しいメモリブロックを割り当てる。

GCはデータ構造やアルゴリズムにとって非常に便利な機能だが、メモリの割り当てとガベージコレクションはコストのかかる操作であり、一部のプログラムの実行を著しく遅くする。

@nogcは、関数がGCを直接または間接的に使用できないことを意味する。

void foo() @nogc {
    // ...
}
D

@nogc関数はGC操作を含まないことをコンパイラが保証する。例えば、次の関数は、@nogc保証を提供しない上記のappend()を呼び出すことはできない。

void foo() @nogc {
    int[] slice;
    // ...
    append(slice);    // ← コンパイルエラー
}
D
エラー: @nogc関数'deneme.foo'は、@nogcではない関数、
'deneme.append'を呼び出すことはできない
Undefined
コード安全属性

@safe@trusted、および@systemは、関数が提供するコードの安全性に関する属性だ。純度と同様に、コンパイラはテンプレート、デリゲート、匿名関数、およびauto関数の安全性のレベルを推論する。

@safe関数

プログラミングエラーの一種として、意図せずにメモリ内の無関係な場所に書き込み、その場所のデータを破損するエラーがある。このようなエラーは、ほとんどの場合、ポインタの使用や型キャストの適用ミスによって発生する。

@safe関数は、メモリを破損する可能性のある操作を含まないことを保証する。コンパイラは、@safe関数で次の操作を許可しない。

@trusted関数

一部の関数は、実際には安全であるにもかかわらず、さまざまな理由により@safeとしてマークできない場合がある。例えば、Cで記述されたライブラリを呼び出す必要がある関数では、その言語では安全性がサポートされていない場合がある。

また、@safeコードでは許可されていない操作を実行する関数もあるが、十分にテストされており、その正しさが信頼されている場合もある。

@trustedは、関数は@safeとマークすることはできないが、安全であるとみなすことをコンパイラに伝える属性だ。コンパイラはプログラマを信頼し、@trustedコードを安全であるかのように扱う。例えば、@safeコードが@trustedコードを呼び出すことを許可する。

@system関数

@safeまたは@trustedとマークされていない関数は、デフォルトの安全属性である@systemとみなされる。

コンパイル時関数実行 (CTFE)

多くのプログラミング言語では、コンパイル時に実行される計算は非常に制限されている。このような計算は、固定長配列の長さを計算したり、単純な算術演算を行うようなシンプルなものである:

writeln(1 + 2);
D

上記の1 + 2式は、3と記述されたものと同じようにコンパイルされ、実行時には計算は行われない。

DにはCTFEがあり、実行可能であれば、あらゆる関数をコンパイル時に実行することができる。

以下のプログラムは、メニューを出力するプログラムだ。

import std.stdio;
import std.string;
import std.range;

string menuLines(string[] choices) {
    string result;

    foreach (i, choice; choices) {
        result ~= format(" %s. %s\n", i + 1, choice);
    }

    return result;
}

string menu(string title,
            string[] choices,
            size_t width) {
    return format("%s\n%s\n%s",
                  title.center(width),
                  '='.repeat(width),    // 水平線
                  menuLines(choices));
}

void main() {
    enum drinks =
        menu("Drinks",
             [ "Coffee", "Tea", "Hot chocolate" ], 20);

    writeln(drinks);
}
D
functions_more.8

同じ結果を異なる方法で実現できるものの、上記のプログラムは、以下のstringを生成するために非自明な操作を実行している:

飲み物
1コーヒー
2紅茶
3ホットチョコレート

drinksのようなenum定数の初期値は、コンパイル時に既知でなければならないことを覚えておいてほしい。この事実だけで、menu()はコンパイル時に実行される。コンパイル時に返される値は、drinksの初期値として使用される。その結果、その値がプログラムに明示的に記述されているかのように、プログラムがコンパイルされる。

// 上記のコードと同等:
enum drinks = "       Drinks       \n"
              "====================\n"
              " 1. Coffee\n"
              " 2. Tea\n"
              " 3. Hot chocolate\n";
D

関数がコンパイル時に実行されるためには、その関数がコンパイル時に実際に必要となる式に現れる必要がある。

明らかに、すべての関数をコンパイル時に実行することは不可能だ。例えば、グローバル変数にアクセスする関数は、グローバル変数は実行時に初めて存在し始めるため、コンパイル時には実行できない。同様に、stdoutは実行時にのみ利用可能なので、表示を行う関数はコンパイル時には実行できない。

__ctfe変数

CTFEの強力な点としては、結果が必要になるタイミングに応じて、コンパイル時と実行時の両方で同じ関数を使用できることが挙げられる。CTFEでは、関数を特別な方法で記述する必要はないが、関数内の操作の中には、コンパイル時または実行時のいずれかでしか意味をなさないものもある。特別な変数__ctfeを使用すると、コンパイル時のみ、または実行時のみに使用するコードを区別することができる。この変数の値は、関数がCTFEで実行されている場合はtrue、それ以外の場合はfalseになる。

import std.stdio;

size_t counter;

int foo() {
    if (!__ctfe) {
        // このコードは実行時に実行される
        ++counter;
    }

    return 42;
}

void main() {
    enum i = foo();
    auto j = foo();
    writefln("foo is called %s times.", counter);
}
D
functions_more.9

counterは実行時にのみ存在するため、コンパイル時には加算することはできない。そのため、上記のコードでは、実行時にのみ加算しようとしている。iの値はコンパイル時に決定され、jの値は実行時に決定されるため、foo()はプログラムの実行中に1回だけ呼び出されたと報告される。

fooが1回呼び出された。
要約