Skip to content

Instantly share code, notes, and snippets.

@k3kaimu
Last active December 19, 2015 21:18
Show Gist options
  • Save k3kaimu/6018917 to your computer and use it in GitHub Desktop.
Save k3kaimu/6018917 to your computer and use it in GitHub Desktop.
011 関数とデリゲート途中まで

関数

関数とは?

関数(function)は、

  1. データを受け取って、
  2. データの加工や、何か処理を行い、
  3. 結果を返す

ものです。

関数が受け取るデータのことを、引数(argument)といい、 関数が返す結果を返り値または戻り値(return value)といいます。

数学での関数f(x, y, z, ...)は、引数が同じであれば、常に同じ結果を返します。 しかし、プログラムの関数はそうではありません。

プログラムの関数では、同じ引数が与えられたとしても、外界の状態によっては計算結果が変わるからです。 たとえば、「コンソールで入力された数をint型で返す」関数readIntがあったとします。 その関数は「何も受け取らず、ただint型を返すような関数」だと定義できます。 この関数の返す値は人間がコンソールに入れる値に左右されます。

関数による処理のまとめ

頻繁に使う処理をまとめて関数にしておくことによってソースコードの可読性やメンテナンス性が向上します。 たとえば、もし、配列の総和を返す関数sumが定義されているなら、総和を計算する箇所ではforeach文の代わりにsum関数を使って書くことができます。

// before: sumを使わない
{
    int s;              // 合計

    foreach(e; arr1)
        s += e;


    int av;             // 平均

    foreach(e; arr2)
        av += e;

    av /= arr2.length;
}


// after: sumを使う
{
    int s = sum(arr1),
        av = sum(arr2) / arr2.length;
}

もし、プログラミング言語に関数という機能がないとしたら、プログラミングという作業は非常につらい作業になったことでしょう。 もしくは、ユーザーは関数を定義できない言語だとしたら、あなたはプログラムを書くことを辞めたくなるはずです。 それほど、関数が行う処理の「隠蔽」と「まとめ」は重要なのです。

関数の基礎

宣言の書き方と関数本体

D言語では、引数リストParameterListを受け取り、ReturnTypeを返す関数を以下のように書きます。 この基本の構文は、C言語やC++などの言語と同じ構文です。

ReturnType functionName(ParameterList)
{
    FunctionBody
}

関数本体(FunctionBody)は省略して、関数プロトタイプのみにすることができます。 その場合には、{FunctionBody}の代わりに;を付けておきます。

ReturnType functionName(ParameterList);

たとえば、int型の値を2つ受け取って、それらの和を返す関数addIntは、次のように書きます。

int addInt(int a, int b)
{
    return a + b;
}

return文はreturn <expr>;という形式をとり、機能は「<expr>を返し、処理を呼び出し元に復帰する文」です。 簡単にいえば、呼び出し元に結果を返してから、その関数を即座に終了させる効果があります。 返り値がある関数では、必ずreturnで値を返して関数を終了させます。

もちろんreturn文は、次のように関数の任意の場所に書くことができます。

int foo(int a)
{
    if(a)
        while(a)
            do
                return a;
            while(a);

    return a;
}

Goto: 問題1 「readIntを実装しよう」
Goto: 問題2 「sumを実装しよう」

関数のすべての条件分岐や最後にreturnが無ければ、コンパイルエラーとなります。 ということは、関数内の絶対に到達し得ない場所にもreturnが必要である、ということになります。 なぜなら、コンパイラでは「絶対に到達し得ない場所」という判断が行えず、また絶対にreturnしなければ、その関数が値を返さずに終了してしまうという事態に陥るからです。

次の状況を想像してみましょう。 関数人であるB君は、同じく関数人であるAさんに愛情(引数)をもらって一生懸命働きます。 しかし、B君はAさんに給料(返り値)を渡しませんでした。 そんな状況は有ってはならないのです。 もちろん、最初から見返りがない(返り値型がvoid)場合はいいのですが。

// 意味のない関数
int foo(int a)
{
    if(a || !a){
        while(a){
            if(a)
                return a;
        }
    }
    
    // ここには絶対到達しない
    // しかし、returnしておかないとコンパイラに怒られる
    return 0;
}

絶対に到達し得ないのにreturn 0;と書いていると、他人が読んだ時に「こいつ何書いてるんだ?」というふうに思われてしまします。 また、return 0;というコードを入れることによって、その関数が失敗したから0を返したのか、成功した結果の0なのかわからなくなります。 よって、このような場合にはreturnの代わりにassert(0);を入れてあげます。

// 意味のない関数
int foo(int a)
{
    if(a || !a){
        while(a){
            if(a)
                return a;
        }
    }
    
    // ここには絶対到達しない
    assert(0);
}

assert(0);があれば、returnがなくてもコンパイルは通ります。 もしそのassert(0);が実行されてしまったとしても次のようなメッセージと共にプログラムはただちに終了します。

core.exception.AssertError@foo(10): Assertion failure
----------------
0x0040323B
0x0040201E
0x0040202A
0x00402633
0x00402231
0x00402054
0x75B933AA in BaseThreadInitThunk
0x772F9EF2 in RtlInitializeExceptionChain
0x772F9EC5 in RtlInitializeExceptionChain
----------------

次の状況を想像してみましょう。 関数人であるB君は、同じく関数人であるAさんに愛情(引数)をもらって一生懸命働きます。 しかし、B君はAさんに給料(返り値)を渡しませんでした。 実は、関数人には爆弾(assert(0);)が仕かけられています。 その爆弾が爆発するのは、恩など(返り値)を返さなかったときです。 つまり、B君は爆発しました。 悲しいことに、B君が爆発してしまったがために給料がもらえなかったAさんは、Aさん自身の仕事を遂行できなくなりました。 その結果、AさんはAさんの親(関数Aの呼び出し元)に給料を送ることができなくなりました。 すると、Aさんの爆弾も爆発し、つまり最終的にはmain関数ちゃんまでもが爆発して、プログラム界は破滅します。

Goto: 問題3 「コンパイルできない!」

何も返さない関数を書きたいのであれば、ReturnTypevoidとします。 そのような関数では、returnを関数中に書く必要はなく、関数を途中で終わらせたい場合にだけreturn;と書きます。 返り値がない関数で、return文が実行されることなく関数の最後まで到達した場合には、return文と同様の効果により関数が終了します。

void foo(int a, int b)
{
    if(a > 0)
        return; // a > 0 の場合には、関数は終わり、即座に処理が呼び出し元に戻る
    else
        writeln(b - a);

    // a <= 0 の場合にはここまで来て、処理が呼び出し元に戻る
}

この説明が分かりにくければ、main関数を思い出してみましょう。 main関数は、ReturnTypevoidな関数でしたが、return文をいちいち入れませんでしたね。 しかし、main関数はちゃんと終了していました。

return文を入れて、main関数を途中で強制的に終わらせることもできます。

import std.conv, std.stdio, std.string;


/// 例:コンソールで入力された数字をint型で返す関数
int readInt()
{
    return readln().strip().to!int();
}


void main()
{
    writeln("main");
    writeln("10以上整数を入力すると終了----");

    if(readInt() >= 10)     // ある条件を満たせば、
        return;             // 終了

    writeln("end");
}

Goto: 問題4 「helpメッセージを表示せよ」

関数の引数

関数は引数を受け取りますが、関数宣言で書かれているint aint b仮引数(parameter)といいます。 逆に、addInt(4, 5)とした場合の45実引数(argument)といわれます。

関数本体が無い場合、もしくは仮引数を関数本体で使わない場合には、仮引数を省略して型だけにすることもできます。

// intを3つ受け取るが、関数本体がないので仮引数は型だけしか書かない
int add(int, int, int);

通常、実引数は仮引数にコピーされて関数に渡されます。 つまり、値型であれば仮引数を変更しても実引数には影響しませんが、参照型であればその参照(住所)をコピーしますから、コピーされた参照を通して参照元に影響を与える可能性があります。

// aは値型
void addToValue(int a, int b)
{
    a += b;
}


// aはポインタ(参照型)
void addToRef(int* a, int b)
{
    *a += b;        // ポインタの参照先のインクリメント
                    // 呼び出し元に影響を与える操作

    a = null;       // ポインタの書き換え
                    // この操作では呼び出し元に影響はない
}


void main()
{
    int m = 2,
        n = 13;

    addToValue(m, n);
    writefln("m: %s, n:%s", m, n);      // 2, 13

    addToRef(&m, n);                    // ポインタ(参照型)を渡す
    writefln("m: %s, n:%s", m, n);      // 15, 13
                                        // m が書き換えられてる!
}

宣言された仮引数の型のリストと実引数の型のリストが一致しなければコンパイル時にエラーがでます。

// test00901.d
int add(int a, int b) { return a + b; }

void main()
{
    int a = add(3, 5),
        b = add(3),             // Error: function test00901.add (int a, int b) is not callable using argument types (int)
        c = add(3, 4, 5),       // Error: function test00901.add (int a, int b) is not callable using argument types (int, int, int)
        d = add(3.0, 4);        // Error: function test00901.add (int a, int b) is not callable using argument types (double, int)
}

デフォルト引数

仮引数にはデフォルト値を設定することができます。 デフォルト値が設定された仮引数に渡す実引数は省略することができます。 省略された場合には、仮引数に設定されたデフォルト値が仮引数の値となります。

しかし、デフォルト値を設定したとしても、その仮引数の後ろにデフォルト値が設定されていない仮引数がある場合にはコンパイルエラーとなります。

int getValue(int* p, size_t idx = 0)
{
    return p[idx];
}


// idxはデフォルト値が設定されているが、後ろにデフォルト値が設定されていない v があるのでエラー
// Error: default argument expected for v
/*
bool getAndTest(int* p, size_t idx = 0, int v)
{
    return p[idx] == v;
}
*/

// デフォルト値は2つ以上の引数にも設定可能
int getValue2d(int** p, size_t i = 0, size_t j = 0)
{
    return p[i][j];
}


void main()
{
    int* p = (new int[10]).ptr;
    foreach(i, ref e; p[0 .. 10])
        e = i;

    p[0 .. 10].reverse;

    writeln(p[0 .. 10]);            // [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

    // idxを指定して呼び出し
    writeln(getValue(p, 4));        // 5

    // idxを省略して呼び出すと、idxは0であると解釈される
    writeln(getValue(p));           // 9

    int** pp = &p;
    writeln(getValue2d(pp));          // 9
}

引数の記憶域クラス

関数の引数にも、普通の変数と同様に記憶域クラス(storage class)を付けることができます。

一切ストレージクラスがついていない引数は、値が(参照型ならその参照が)コピーされます。 これに対して、refout, lazyは特殊な渡され方をされます。

  • const

    constが付けられた仮引数の型はconst(Type)となり、その仮引数は書き換え不可能になります。 const T argconst(T) argは同じ意味です。

    constな引数は、mutableな値(const, immutableではない)でも非mutableな値(constimmutable)でも、受け取ることができます。

    (「mutableな」とは、「変更可能な」という意味です。)

    const型の解説

    int getValue(const int* p)
    {
        //*p += 3;                  // pはconst(int*)型、*pはconst(int)型なので書き換え不可
    
        return *p;                  // *pはconst(int)型なので、int型として返せる
    }
  • immutable

    immutableストレージクラスとなっている引数はその引数の型がimmutable(Type)となります。

    immutableストレージクラスな引数はimmutableな値しか受け付けません。 もちろん、引数はコピーされるためimmutableでない値型も受け付けます。 値型であればimmutableなコピーを作ることができるからです。

    immutable型の解説

    immutable(int)* getValue(immutable int* p)
    {
        //*p += 3;                  // pはimmutable(int*)型、*pはimmutable(int*)型なので書き換え不可
    
        return *p;                  // *pはimmutable(int*)型なので、immutable(int)*型として返せる
    }
  • inout

    このストレージクラスとなった引数はinout型になります。 仮引数にinout型を一つでも含む関数はinout関数と呼ばれます。 inout関数は、その関数を呼び出す実引数によって関数の返り値の型が変わります。

    まずは仮引数にinout型を1つだけ含む関数inout(int[]) getFront(inout(int[]) x);について考えてみましょう。 この関数にはint[]const(int[])、さらにはimmutable(int[])型の値を渡すことが出来ます。 getFront関数の返り値は、実引数がint[]の場合にはint[]が、const(int[])の場合にはconst(int[])が、そしてimmutable(int[])の場合にはimmutable(int[])になります。

    コンパイラが3つのパターンについてgetFront関数を生成しているわけではないことに注意しましょう。 コンパイラは、呼び出し毎に、実引数の型を調査して、それに見合った返り値の型を設定しているのです。

    次に、仮引数にinout型が1つよりも多く存在した場合ですが、コンパイラは呼び出し毎にすべてのinout仮引数に対する実引数を調査します。 コンパイラによる実引数の調査の結果、コンパイラは次のように返り値の型を変更します。

    1. inoutに対応する型がすべてimmutableであれば返り値のinoutimmutableに置き換わった型になります。
    2. inoutに対応するすべての実引数の型がmutable(constでもimmutableでもない)であれば、返り値のinoutは取り除かれます。
    3. 1や2にマッチしなかった場合は返り値のinoutconstに置き換わります。
    inout(int)[] foo(inout(int[]) x, inout(int[]) y)
    {
        return x ~ y;
    }
    
    
    void main()
    {
        int[] marr = [1, 2, 3];
        const carr = marr;
        immutable iarr = marr.dup;
    
        // (mutable, mutable) => mutable
        auto a = foo(marr, marr);
        static assert(is(typeof(a) == int[]));
    
        // (mutable, const) => const
        auto b = foo(marr, carr);
        static assert(is(typeof(b)== const(int)[]));
    
        // (mutable, immutable) => const
        auto c = foo(marr, iarr);
        static assert(is(typeof(c) == const(int)[]));
    
        // (const, immutable) => const
        auto d = foo(carr, iarr);
        static assert(is(typeof(d) == const(int)[]));
    
        // (immutable, immutable) => immutable
        auto e = foo(iarr, iarr);
        static assert(is(typeof(e) == immutable(int)[]));
    }

    また、inout関数内でのみinout型の変数を宣言できます。 inout(T)という型はconst(T)へは暗黙変換可能ですが、Timmutable(T)へは暗黙変換できません。

    import std.traits;
    
    void foo(inout(int)[] x)
    {
        inout(int)[] y = x;
        auto z = x ~ y;
        static assert(is(typeof(z) == inout(int)[]));
    
        //int[] mz = z;     Error
        const cz = z;
        //immutable iz = z; Error
    
        // inout と mutable の CommonType => const
        static assert(is(CommonType!(inout(int)[], int[])
            == const(int)[]));
    
        // inout と const の CommonType => const
        static assert(is(CommonType!(inout(int)[], const(int)[])
            == const(int)[]));
    
        // inout と immutable の CommonType => inout(const(int))
        static assert(is(CommonType!(inout(int)[], immutable(int)[])
            == inout(const(int))[]));
    }
  • shared

    このストレージクラスとなっている引数の型は、shared型になります。

    shared型の解説

  • scope

    scopeが付いた引数は、その引数が持つ参照をその関数の外部に移動することができなくなります。 つまり、値型な引数にscopeをつけても意味はありませんが、スライスやデリゲート、クラスなどのような参照を持つ型はグローバル変数に代入したり、関数から返すことはできなくなります。

    int[] gSlice;               // global変数
    
    int[] foo(scope int[] x)
    {
        gSlice = x;             // コンパイルエラー
        return x;               // コンパイルエラー
    }

    ただし、現在のdmd(dmd 2.063.2)では、このscopeは機能していないようで、上記のようなコードもコンパイルが通ってしまいます。

    bugzilla

  • in

    const scopeと等しくなります。 よって、const同様に変更ができなくなります。 参照型に対しては、参照を関数外部に持っていくことも出来ないようになります。

    ただし、現在のdmd(dmd 2.063.2)ではscopeストレージクラスは機能してないようなので、constに等価なストレージクラス?[要出典]

  • ref

    実引数として左辺値を受け取り、仮引数への操作はすべて受け取った実引数への操作になります。 つまり、左辺値を関数内で操作し、関数を超えてその左辺値に影響を与えたい場合に利用します。

    int moveFront(ref int[] arr)
    {
        auto dst = arr[0];
        
        arr = arr[1 .. $];
        return dst;
    }
    
    
    void main()
    {
        int[] arr = [0, 1, 2, 3];
        writeln(moveFront(arr));        // 0
        writeln(arr.length);            // 3
                                        // arrが変更されている
    
        writeln(moveFront(arr));        // 1
        writeln(arr.length);            // 2
                                        // arrが変更されている
    }
  • auto ref

    このストレージクラスは、テンプレート関数でのみ使用可能になります。 なので今は気にする必要はありませんが、機能としては「refで引数が取れるならrefでとる」というストレージクラスです。 つまり、呼び出した際の実引数が左辺値であれば参照refで受け取って、右辺値ならnon-refで受け取ります。

  • out

    左辺値を受け取るという特性はrefと同じですが、関数に入る時点でその参照の値がデフォルト初期化値.initで初期化され、以降はrefと同様の動作になります。

    返り値以外に出力をしたい場合に使用します。

    int findMax(in int[] arr, out size_t idx)
    {
        foreach(i, e; arr)
            if(e > arr[idx])
                idx = i;
        return arr[idx];
    }
    
    
    void main()
    {
        // どうせ、findMax呼び出し時に初期化されるからvoidでもよい
        size_t idx = void;
    
        writeln(findMax([0, 1, 5, 1, 0], idx));     // 5
        writeln(idx);                               // 2
    }
  • lazy

    このストレージクラスでは、実引数の評価は遅延評価され、関数内で必要になった時に初めて評価されます。

    仕組みとしては、引数を返すデリゲートを作り、そのデリゲートを呼び出しています。 つまり、foo(expr)foo((){return expr;})になります。 デリゲートについては、後ほど説明するので、「遅延評価され、仮引数を複数回評価すると、実引数も複数回評価される」とだけ覚えておいてください。

    int get(int* p, lazy int defValue)
    {
        return p ? *p : defValue;
    }
    
    
    void main()
    {
        int a = 3;
    
        writeln(get(&a, ++a));      // 3
                                    // get(&a, (){ return ++a; })に等価
    
        writeln(get(null, ++a));    // 4
                                    // get(null, (){ return ++a; })に等価
    }

    次のように複数回評価することもできます。

    import std.array;
    import std.stdio;
    
    
    int[] callN(lazy int v, size_t n)
    {
        // 配列に要素を追加していく場合には、std.array.appenderを使う
        auto app = appender!(int[])();
    
        // n回評価して、追加していく
        foreach(unused; 0 .. n)
            app.put(v);
    
        return app.data;            // appenderが管理している配列を返す
    }
    
    
    void main()
    {
        int a = 3;
    
        writeln(callN(++a, 3));               // [4, 5, 6]
    }

    lazyがどれほど素晴らしい機能なのかを体験するために、次のソースコードをコンパイルして実行してみましょう。

    import std.stdio;
    import std.datetime;
    
    int tarai_lazy(int x, int y, lazy int z)
    {
        if (x <= y) return y;
        return tarai_lazy(tarai_lazy(x-1, y, z), tarai_lazy(y-1, z, x), tarai_lazy(z-1, x, y));
    }
    
    
    int tarai(int x, int y, int z)
    {
        if (x <= y) return y;
        return tarai(tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y));
    }
    
    
    void main()
    {
        {
            auto mt = measureTime!(a => writefln("non-lazy: %s[usecs]", a.usecs))();
            tarai(10, 8, 0);
        }
    
        {
            auto mt = measureTime!(a => writefln("lazy: %s[usecs]", a.usecs))();
            tarai_lazy(10, 8, 0);
        }
    }

    実行結果はどのように出ましたか? 私の環境では、以下のように出力されました。

    non-lazy: 3344[usecs]
    lazy: 1[usecs]
    

    先ほどのプログラムは、「たらい回し関数(竹内関数)」のD言語での実装でした。 たらい回し関数は、関数自体が短く、引数x, y, zが小さな数であったとしても、計算量が膨大な数になってしまう関数です。 私の環境だとtarai(10, 8, 0)に3ミリ秒程度かかったということになります。

    しかし、遅延評価バージョン(tarai_lazy(10, 8, 0))では、たった1マイクロ秒で計算が終わってます。 taraitarai_lazyの違いは、引数がlazy int zになってるだけです。 たらい回し関数は、zが遅延評価されると途端に計算量が低下する関数なので、このようにtarai_lazyは高速なのです。

可変個引数関数

引数に取りたい実引数の数が、実行条件によって変わることがあります。 たとえば、writelnwriteflnなどのwrite系の関数は、引数をいくらでも取ることができます。

このような関数を作るのには、様々な方法があります。

  • 同じ型の引数を可変個取りたい場合

    たとえば、次のように文字列をいくつか受け取って、それらを連結した文字列を返す関数は、次のように書けます。

    string chainString(string[] str...)
    {
        string chained;
    
        foreach(e; str)
            chained ~= e;
    
        return chained;
    }
    
    
    void main()
    {
        writeln(chainString());
        writeln(chainString("foo"));
        writeln(chainString("foo", "bar"));
        writeln(chainString("foo", "bar", "hoge"));
    }

    可変長パラメータであるstrに、引数のリストが入ります。 strを関数外に移動することは不正です (つまり、scopeが暗黙的に付いていると考えれる?[要出典])。

    実際には、静的配列にすることもできます。 たとえば、いくつかの数を受け取って、その中で最も大きな整数を返す関数は次のように書けます。

    T max(T, size_t N)(T[N] nums...)
    if(N > 0)
    {
        T v = nums[0];
    
        foreach(e; nums[1 .. $])
            v = v > e ? v : e;
    
        return v;
    }
    
    
    void main()
    {
        writeln(max(0, 1, 2, 3));       // 3
    }

    この関数は、テンプレート関数(Template Function)といい、任意の型Tと0より大きい任意のNに対してマッチするテンプレート関数です。

  • 異なる型の引数を可変個取りたい場合

    writelnwriteflnなどは異なる型の引数を任意個取ることができます。 このような関数は可変個引数関数と呼ばれ、普通はテンプレートを使って作ります。

    void main()
    {
        println(" : ", "foo", "bar", 2, 4);     // foo : bar : 2 : 4
    }
    
    
    void println(T...)(string sep, T values)
    {
        // valuesはforeachで回せる
        foreach(i, e; values){
            if(i != 0)
                write(sep);
    
            write(e);
        }
    
        writeln();
    
        /* Tもforeachで回せる
        foreach(i, Unused; values){
            write(values[i]);
    
            if(i != T.length - 1)
                write(sep);
        }
    
        writeln();
        */
    }

    これについてはテンプレートの章で説明するとして、今回は全く使われない方法で可変個引数関数を作ります。 この方法にはCスタイルとDスタイルがありますが、全く使う機会がないのでさらっと流してしまいます。 詳しい仕様を知りたい場合には、可変個引数 - プログラミング言語Dを読みましょう。

    • Cスタイルな可変個引数関数

      関数の宣言は以下のようになります。

      extern(C) void foo(int a, ...);
      // extern(C) void bar(...);             // エラー

      ...の部分が可変個の引数を受け取れる部分です。 関数引数が...だけではいけません。 最低1つは可変個でない引数が必要です。 ちなみに、extern(C)は、「この関数はC言語みたいな関数だよ」ということです。

    • Dスタイルな可変個引数関数

      void foo(int a, ...);
      void bar(...);                          // OK

      Dスタイルの可変個引数関数では、_argptr_argumentsという変数にアクセスできます。 import core.vararg;とし、va_arg!T(_argptr)で型Tの引数を取ることが出来ます。 また、va_argで引数を取ったあと次にva_argを呼び出す場合は、その次の変数が読み出せます。

      _argumentsという引数には、可変個引数部分の引数の型の情報が格納されています。 型はTypeInfo[]で、if(_arguments[i] == typeid(int)){}のように、i番目の引数の型がint型かどうか比較ができます。

      これから例として先ほどのprintln関数を作りたいのですが、あらためて引数とその動作を以下に示します。

      println(" : ", "foo", "bar", 2, 4);     // foo : bar : 2 : 4
      println(", ", 1, 2, 'c', "foo");        // 1, 2, c, foo

      対応する型は簡単化のためにint, char, stringでいいでしょう。 さて、print関数の動作と仕様がわかったので、実装していきたいと思います。

      _argumentsTypeInfo[]ですから、foreachで回すのが適切でしょう。 各要素で型を判別して、va_arg!Tで引数を取得します。 よって、実装は次のようになります。

      import core.vararg;
      import std.stdio;       // write, writelnを使うから
      
      void println(string sep, ...)
      {
          foreach(i, type; _arguments){
              if(type == typeid(int))
                  write(va_arg!int(_argptr));
              else if(type == typeid(char))
                  write(va_arg!char(_argptr));
              else if(type == typeid(string))
                  write(va_arg!string(_argptr));
              else
                  assert(0);
      
              if(i != _arguments.length - 1)
                  write(sep);
          }
      
          writeln();
      }

オブジェクトを形成する引数

関数に渡されたの引数で、クラスのコンストラクタを走らせ、インスタンスを組み立てることができます。

class Foo{ this(int x, int y){} }

// Fooのコンストラクタが呼ばれる
void foo(Foo foo...)    // fooには暗黙的にscopeが付いているようなもの
{
    writeln(foo);
}


void main()
{
    foo(1, 2);  // Fooのコンストラクタは(int, int)
}

返値型推論

関数の返り値の型が複雑で長くなる場合があります。 その場合は、返り値の型をautoと書いておけば、return文から返り値の型が推論されるようになり便利です。

また、auto refとすることで、参照で返すかどうかも推論されます。

import std.algorithm;

// この関数の返り値の型は MapResult!(unaryFun, FilterResult!(unaryFun, int[]))
auto func(int[] arr)
{
    return arr.filter!"a > 2"().map!"a >> 1"();
}


// 引数が左辺値(lvalue)なら、返り値もlvalue
auto ref add1()(auto ref int x)         // 仮引数ストレージクラスのauto refは、テンプレート関数専用なので`()`が必要
{
    x += 1;
    return x;
}


void main()
{
    int a;
    ++add1(a);      // 返り値がlvalueなので、インクリメントできる
    writeln(a);     // 2

    //++add1(10);   // Error: add1(10) is not an lvalue
                    // 10は右辺値(rvalue)なので、lvalueで返せない
}

関数の属性

関数に属性をつけることで、コンパイラにその関数の情報を与えることができます。 たとえば、@propertyという属性を、引数が0個の関数に付けると、()を省略して呼び出すことができます。 また、外部に影響を与えないということが静的に保証されている関数にはpureが付きます。

プロパティ関数@property

引数の数が0, 1, 2個の場合にのみ有効な属性です。 この属性が付いた関数は、次のような構文で関数を呼び出すことができるようになります。

import std.stdio;


// 引数の数が0個のプロパティ関数
int foo() @property
{
    return 1;
}


// 引数の数が1つのプロパティ関数
int bar(int a) @property
{
    return a;
}


// 引数の数が2つのプロパティ関数
int add(int a, int b) @property
{
    return a + b;
}


//Error: properties can only have zero, one, or two parameter
/*
int tri(int a, int b, int c) @property pure nothrow @safe
{
    return a + b + c;
}
*/


void main()
{
    writeln(foo);               // 1
                                // 引数の数が0の@property関数はカッコ()無しで変数みたいに呼べる
    writeln(foo());             // もちろん、カッコ付きで読んでもOK

    writeln(bar = 12);          // 12
                                // 引数の数が1つだと、プロパティ関数がさも代入されるかのような構文で呼べる
                                // この場合は、bar(12)に等価
    writeln(12.bar);            // UFCSとプロパティ関数の組み合わせ

    writeln(1.add = 15);        // 16
                                // 引数の数が2つだと、first.func = secondのような構文でも呼べるようになる。
                                // この場合はadd(1, 15)に等しい
}

プロパティ関数でない関数であっても、プロパティ関数のように呼び出すことは可能です。 しかし、dmdでは-propertyを指定することで、プロパティ関数でない関数がプロパティ関数のような構文で呼ばれている箇所がエラーになります。

構造体やクラスについては後の章で説明しますが、それらのメンバ関数がプロパティ関数の場合にも同様に呼び出すことができます。

import std.stdio;


struct S
{
    // 引数の数は0個
    int foo() @property
    {
        return 1;
    }


    // 引数の数は1個
    int bar(int a) @property
    {
        return a;
    }
}


void main()
{
    S s;

    writeln(s.foo);             // 1
                                // メンバ関数の場合でも、引数の数が0個なら、関数呼び出しのカッコが省略できる
    writeln(s.foo());           // もちろん、カッコ付きで読んでもOK

    writeln(s.bar = 12);        // 12
                                // メンバ関数の場合でも、引数の数が1つだと、さも代入されるかのような構文で呼べる
                                // この場合は、s.bar(12)に等価
}

関数のメモリ安全性

D言語には、

  • 未定義動作を引き起こさないこと(メモリ安全性といいます)を保証する
  • メモリ安全性を保証できない場合でも、そのようなコードを検証しやすくする

ための仕組みが用意されています。

関数に後述する属性を付けることでこの仕組みを利用することができ、未定義動作が原因の不可解で再現性のないバグを防ぐことができます。
メモリ安全性についての属性は3つあり、付けられた属性によって、関数を以下のように分類することができます。

  • セーフ関数@safe

    セーフ関数(safe function)は、その関数内でのすべての操作がメモリ安全な関数で、未定義動作を引き起こさないことがコンパイラによって保証されます。 そのため、次のような制約があります。

    • インラインアセンブラは書けない
    • castによって、constimmutable, sharedを取り除くことができない
    • castによって、immutablesharedを付加することができない
    • castによって、ポインタ型T*を、void*以外の他のポインタ型U*へ変換できない
    • castによる、ポインタでない型から、ポインタ型へ変換できない
    • ポインタ値の変更(加算, 減算, ...etc)不可
    • ポインタが指している要素以外は触れない(ptr[idx]は不可)
    • ポインタ型を含むunionは触れない
    • class Exception派生でない例外のcatchができない
    • システム関数(後述)の呼び出しができない
    • ローカル変数や関数引数へのアドレスの取得ができない
    • __gsharedな変数を触ることができない

    正確には、以下を参照:
    関数#safe-functions - プログラミング言語 D (日本語訳)
    Functions#safe-functions - D Programming Language
    SafeD - プログラミング言語 D (日本語訳)
    SafeD - D Programming Language

    セーフ関数はコンパイル時に解析され、セーフ関数であるのにメモリ安全でない操作をしている場合には、コンパイルエラーとなります。

    int foo(int* p) /*@safe*/
    {
        return p[1];            // *(p + 1)なので、fooはセーフ関数になれない
    }
    
    
    int foo_safe(int* p) @safe
    {
        return p[0];            // *pと等価なのでOK
    }
    
    
    int bar(int[] arr) @safe
    {
        size_t idx = 1;
        return arr[idx];        // 配列(スライス)に対するidxアクセスはOK
    }
  • 信頼済み関数@trusted

    信頼済み関数(trusted function)は、関数内ではメモリ安全ではない操作を行なっているけれども関数全体としてみれば安全であるような関数です。 信頼済み関数では、操作の静的な制約はありませんが、メモリ安全であることをプログラマが保証しなければいけません。 このため、関数を定義するプログラマは未定義動作を引き起こさないように注意する必要があります。

    int foo(int[] arr) @trusted
    {
        return arr.ptr[arr.length - 1];   // ポインタが指している要素以外に触っているので、fooはセーフ関数になれない
                                // しかし、プログラマが保証するならば、信頼済み関数になれる
    }
    
    
    int foo_safe(int* p) @safe
    {
        return p[0];
    }
  • システム関数@system

    システム関数(system function)は、@safeでも@trustedでもない関数です。属性に@systemを付けることで、システム関数であることを明示することもできます。 システム関数は、操作の静的な制約がなく、メモリ安全であることを誰も保証してくれないので、これらの関数を定義するときや使用するときには未定義動作を引き起こさないように注意する必要があります。

    int foo(){}             // デフォルトではsystem関数
    int hoge() @system {}   // 明示的にsystem関数であることを表す
    
    int bar() @safe {}      // system関数じゃなくて、safe関数
    int baz() @trusted {}   // system関数じゃなくて、trusted関数

純粋関数pure

純粋関数とは、その関数が外部に一切の影響を与えないことが静的に保証されている関数です。 つまり、I/O(入出力)は禁止、グローバル変数やネスト関数の場合には外のスコープも触ってはいけません。 もちろん、impureな関数(pureでない関数)を呼び出すことはできません。

int globalValue;

immutable int imm;
const int* cptr;

void foo(int x, int y) pure
{
    //globalValue = x;          // NG
                                // グローバル変数の書き換えは不可

    x = imm;                    // OK
                                // immutableなグローバル変数の読み込みは可能

    //x = *cptr;                // NG
                                // constなポインタは、ポインタ値はconstだが、値は変化するので、読み込み不可

    static int z;
    //z = x;                    // NG
                                // static変数の書き換えは不可

    throw new Exception("例外はOK");

    int[] arr = new int[x];     // newはOK
}

例外を投げない関数nothrow

例外についてはまだ説明していませんが、例外とは、プログラムがある処理をしている最中に起こった異常や、その異常を知らせるメッセージのことです。 「例外を投げる」とは、「異常が発生したというメッセージを発行する」ということになります。 例外はthrow ex;で投げることができ、catchされるまで関数を遡っていきます。 main関数までさかのぼり、最終的にcatchされなければプログラムは終了します。

ToDo: 例外の章へ

nothrow関数は、そんな例外を絶対に投げないことが静的に保証されている関数です。 また、例外は関数を貫いて伝搬するため、nothrow関数内ではnothrow関数しか呼ぶことが出来ません。

void bar(){}                                // nothrow関数でない

void foo() nothrow
{
    //throw new Exception("exception");     // nothrow関数内では例外を投げれない
    //bar();                                // barはnothrow関数でないので、呼べない
}

例外を投げる可能性のある操作を関数内部に持っていても、その操作がtry文中にあり、例外が関数外部にもれないのであれば構いません。

void bar(){}                                // nothrow関数でない

void foo() nothrow
{
    try{
        throw new Exception("exception:");  // tryの中にあるのでOK
        bar();                              // 同上
    }
    catch(Exception ex){}
}

ちなみに、整数の0除算や配列の範囲外参照, assertの失敗では、すべてエラーが投げられますが、これは例外ではないので、nothrow関数内でこれらの操作を行うことは可能です。

void foo() nothrow
{
    throw new Error("error");               // OK
                                            // 例外じゃなくてエラー

    int[] arr;
    auto b = arr[1];                        // エラーが投げられるが、例外でないのでOK

    b /= 0;                                 // エラーが投げられるが、例外でないのでOK
}

UDA(User Defined Attribute)

ToDo: UDAの章へ

const, immutable, inout, abstract, final

これらの属性は構造体structやクラスclassのメンバー関数でのみ使用することができます。

ToDo: 共用体の章へ ToDo: 構造体の章へ ToDo: クラスの章へ

関数オーバーロード(多重定義, overload)

D言語の関数は、引数が違えば、同じ関数名の関数を宣言することができます。

たとえば、C言語には「データをフォーマット指定して文字列に書き込みを行う」関数がstdio.hに以下のように複数あります。 それぞれは引数の型だけがことなるだけで、それらの関数の意味はすべて同じです。 しかし、C言語には関数のオーバーロードという機能がないので、各関数の名前が被ってはいけないという言語仕様上の制約があります。 ですから、sprintf系の関数では、その引数に応じて、先頭にvnを付けて呼び出す関数を区別してやる必要があります。

// Cでのsprintf系
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t n, const char *format, ...);
int vsprintf(char *str, const char *format, va_list arg);
int vsnprintf(char *str, size_t n, const char *format, va_list arg);

逆に、関数オーバーロードの機能があるD言語では、これらの関数は次のように、すべてsprintfという関数名で宣言することが可能です。

// もし、Dでsprintf系をつくるならば
int sprintf(char* str, const char* format, ...);
int sprintf(char* str, size_t n, const char* format, ...);
int sprintf(char* str, const char* format, void* argptr, TypeInfo[] arguments);
int sprintf(char* str, size_t n, const char* format, void* argptr, TypeInfo[] arguments);

呼び出すときは引数にもっともマッチした関数が呼ばれます。 「もっともマッチした関数」とは、以下の優先順位でもっとも高い関数です。

  1. 完全にマッチしている
  2. const付きでマッチしている
  3. 暗黙の型変換によるマッチ
  4. マッチしていない
import std.stdio;

void foo(int){ writeln("int"); }
void foo(in int){ writeln("in int"); }      // in は const scope のこと

void bar(in int){ writeln("in int"); }
void bar(long){ writeln("long"); }

void hoge(float){ writeln("float"); }
void hoge(double){ writeln("double"); }

void main()
{
    foo(1);                 // int
    foo(cast(const)1);      // in int

    bar(1);                 // in int
                            // 暗黙変換よりもconstは優先される
    bar(cast(long)1);       // long

    hoge(1.0L);           // コンパイルエラー:realはfloat, doubleの両方に等しく暗黙変換可能
    /*
    example.d(21): Error: function foo.hoge called with argument types:
        ((real))
    matches both:
        example.d(9): foo.hoge(float _param_0)
    and:
        example.d(10): foo.hoge(double _param_0)
    */
}

同一名称の関数が異なるモジュールに属している際には、コンパイラによる最適な関数の選択方法は複雑になります。 関数の呼び出しがあると、コンパイラはまずはモジュール毎にその関数のオーバーロード集合(overload set)を形成します。 次のステージでは、それぞれのモジュールでもっともマッチする関数を選択します。 前ステージでのマッチする関数の合計がただ一つの場合、つまりは、ただひとつのモジュールだけしかマッチしなければ、そのマッチした関数が呼ばれます。 そうでなければ(複数のモジュールでマッチしたのなら)、コンパイルエラーとなります。

// foo1.d
import std.stdio;

void foo(int){ writeln("foo1.foo(int)"); }
void foo(in int){ writeln("foo1.foo(in int)"); }
// foo2.d
import std.stdio;

void foo(long){ writeln("foo2.foo(long)"); }
void foo(real){ writeln("foo2.foo(real)"); }
// main.d

import foo1, foo2;

void main()
{
    //foo(1);       // Error: foo2.foo at foo2.d(4) conflicts with foo1.foo at foo1.d(4)
                    // モジュールfoo1ではfoo(int)が、foo2ではfoo(long)がマッチし、
                    // 結果的に2つ以上のモジュールでマッチしたのでエラー

    foo(long.max);  // foo2.foo(long)
    foo(1.0);       // foo2.foo(real)
                    // 上記2つともに、モジュールfoo2でのみマッチする
}

もし、foo1foo2に分けられたオーバーロード集合を一つに結合したい場合には、次のようにaliasを使います。

import foo1, foo2;

// モジュールfoo1とfoo2の、fooに関するオーバーロード集合を一つに結合する
alias foo = foo1.foo;
alias foo = foo2.foo;

void main()
{
    foo(1);         // foo1.foo(int)
    foo(long.max);  // foo2.foo(long)
    foo(1.0);       // foo2.foo(real)
}

オーバーロード集合を結合せずとも、明示的に所属するモジュールを指定してやることで解決します。

import foo1, foo2;

void main()
{
    foo1.foo(1);         // foo1.foo(int)
    foo2.foo(long.max);  // foo2.foo(long)
    foo2.foo(1.0);       // foo2.foo(real)
}

ローカルstatic変数

関数内にはstaticと付いた変数を宣言することができます。 静的変数は「その関数だけが触れるグローバル変数」となります。

void foo()
{
    static int cnt;

    writefln("%s回目の呼び出し", ++cnt);
}


void main()
{
    foo();          // 1回目の呼び出し
    foo();          // 2回目の呼び出し
    foo();          // 3回目の呼び出し
    foo();          // 4回目の呼び出し
}

ローカルstatic変数を初期化するには、初期化値がコンパイル時定数である必要があります。 つまり、実行時に決まるような値で初期化できません。 このような場合はstatic bool firstCallというような変数を用いて、初期化しましょう。

string foo(string line)
{
    static bool firstCall = true;       // リテラルはコンパイル時定数
    static int hold/* = line*/;         // ローカル変数や仮引数はコンパイル時定数ではない

    // 第一回目の関数呼び出しのときにのみ中の文が実行される
    if(firstCall){
        hold = line;
        firstCall = !firstCall;
    }

    return hold;
}

Goto: 問題5 「Grand Total」
Goto: 問題6 「Tagged Grand Total」

ネスト関数

なんと関数内には関数を記述できます! また、その関数は外側の関数のシンボルを参照することができます。 もし、ネスト関数がstaticであれば、その外部の関数のstaticなものしか参照できません。

void main()
{
    int a;
    static int s;

    void inc(){ ++a; }

    static void inc_static(){ ++s; }    // staticなものだけ触れる

    writeln(a);         // 0
    inc();
    inc();
    writeln(a);         // 2
}

関数ポインタ

関数を変数に代入して持ち運べたり、違う関数に渡せると嬉しくないですか? 実は、関数ポインタ型というデータ型が存在し、この型へ関数へのポインタを格納しておけば、関数への参照を持ち運ぶことができます。 関数ポインタの型は、ReturnType function(ParameterList)となります。

void foo(int a){ writeln("foo !!!"); }
void bar(int b){ writeln("bar !!!"); }

void main()
{
    // intを受け取る関数を参照する型
    void function(int) fptr = &foo;

    fptr(0);                    // foo !!!

    fptr = &bar;
    fptr(0);                    // bar !!!
}

関数ポインタを使用すれば、関数を値として扱えます。 そのため、条件によって実行する関数を変えたり、関数に関数を渡せたり、関数から関数を返すことも作成可能です。

import std.stdio;

int sum(int a, int b){ return a + b; }
int prd(int a, int b){ return a * b; }


/// std.algorithm.reduceと同じような関数
int reduce(int ini, int[] arr, int function(int, int) f)
{
    while(arr.length){
        ini = f(ini, arr[0]);
        arr = arr[1 .. $];
    }

    return ini;
}


// 状態stateによって、返す関数を変える関数
int function(int, int) getFunc(bool state)
{
    if(!state)      // falseのとき
        return &sum;
    else
        return &prd;
}


void main()
{
    writeln(reduce(0, [1, 2, 3, 4], &sum));     // 10
                                                // 総和

    writeln(reduce(1, [1, 2, 3, 4], &prd));     // 24
                                                // 総乗

    writeln(getFunc(false) == &sum);
    writeln(getFunc(true) == &prd);
}

すべての関数に対して、&<function>が関数ポインタを返すわけではありません。 非staticなネスト関数やメンバ関数(メソッド)についてはデリゲートというものを返します。

デリゲートdelegate

関数ポインタを使えば、たしかに関数から関数を返すことは可能です。 では、引数int aを取り、「引数int bを取って、abの和を返す関数」を返す関数accumを作れるでしょうか? つまり、次のようなコードを満たす関数です。

// accum関数は引数を一つ取って、関数を返す
auto func1 = accum(5);
writeln(func1(3));          // 8
                            // 5 + 3

auto func2 = accum(8);
writeln(func2(12));         // 20
                            // 12 + 8

writeln(func1(3));          // 8
                            // 5 + 3
                            // func1の状態は、func2に影響されない

グローバル変数に最初の引数の値を保存すれば実現できそうですが、func1の状態がfunc2に影響されてしまします。

import std.stdio;


int a;  // accumで、第一引数を保存する変数


auto accum(int a)
{
    // グローバルなaにローカルなaを代入
    .a = a;

    // accumImplへの関数ポインタを返す
    return &accumImpl;
}


int accumImpl(int b)
{
    // グローバルなaとローカルのbの和を返す
    return .a + b;
}


void main()
{
    // accum関数は引数を一つ取って、関数を返す
    auto func1 = accum(5);
    writeln(func1(3));          // 8
                                // 5 + 3

    auto func2 = accum(8);
    writeln(func2(12));         // 20
                                // 8 + 12

    writeln(func1(3));          // 11
                                // 5 + 3 = 8 なのに、func2の影響を受けて、
                                // 8 + 3 = 11 になってしまった。
}

グローバル変数の代わりにstatic変数を使ってもこのような関数は作れないのですが、では関数のローカル変数を触れるネスト関数を作り、その関数ポインタを返すのはどうでしょう?この実装だと、仕様を満たす関数になります。

auto accum(int a)
{
    int accumImpl(int b)
    {
        return a + b;
    }

    return &accumImpl;
}

実は、accumは関数ポインタを返すのではなくて、デリゲート(delegate)というものを返しています。 試しに、返り値の推論をやめてint function(int) accum(int a)と書けばコンパイルエラーになりますね。

Error: cannot implicitly convert expression (&accumImpl) of type int delegate(int b) to int function(int)

コンパイラがいうには、「(&accumImpl)int delegate(int)型であって、int function(int)型には暗黙変換できませんよ」ということなのです。 int delegate(int)型はintを受け取ってintを返すデリゲート型のことです。

デリゲートは、関数ポインタと、それが作られた環境についての情報(スタックポインタ)を併せて持っています。 そのため、accumImplから作られたデリゲートはaccumaの値を参照できるのです。 このaの寿命は、accum関数が終了しても継続し続け、accumImplから作られたデリゲートや、そのデリゲートのコピーがすべて無くなったら、次のガベージコレクタの回収時に回収されます。
(このようなデリゲートをクロージャ(closure)と呼びます。)

accumを2回呼び出し、その2つの返り値のデリゲートが持っているスタックポインタを比較すると、それらは異なります。 つまり、accumの環境(スタック)の複製をデリゲートは持ちます。 このような性質により、accumを何回呼び出したとしてもメモリがある限り、返されるデリゲートは独立します。

A型を受け取り、B型を返すデリゲート」の型はB delegate(A)となります。

関数オブジェクト(関数ポインタや、opCallの定義 されている構造体やクラス)をデリゲートに変換したい場合には、std.functional.toDelegateを使います。

import std.functional;

int foo(int a){ return a; }

void main()
{
    int delegate(int) dlg = toDelegate(&foo);   // 関数ポインタ -> デリゲート
    writeln(dlg(3));                            // 3
}

Goto: 問題7 「カウンター」

関数のリテラルとラムダ

先の例では、関数内にネスト関数を宣言し、そのネスト関数から作られるデリゲートを返していました。 しかし、関数(関数ポインタやデリゲート)がリテラルとしてソースコード中に表現できるなら、わざわざネスト関数を宣言する必要はありませんね。

今回は先ほどのaccumをなるべく短く実装していきましょう。 ネスト関数を使ったaccumを以下にもう一度示しておきます。

int delegate(int) accum(int a)
{
    int accumImpl(int b)
    {
        return a + b;
    }

    return &accumImpl;
}

まず、accumImplをリテラルで表現してみると次のようになります。

int delegate(int) accum(int a)
{
    return delegate int(int b){ return a + b; };
}

行数が極端に減りましたね。 もし、関数ポインタを返したいなら、delegatefunctionにしますが、関数ポインタでは外部の環境(a)へアクセスできないので、今回の場合は関数ポインタにできません。

リテラル表現では、delegateや返り値のintを省くことができます。 すると、次のようにさらに短くなります。

int delegate(int) accum(int a)
{
    return (int b){ return a + b; }
}

このようなリテラルの場合には、関数ポインタかデリゲートかどうかが推論されます。 今回の場合には、外部のaをリテラル内で触っているので、もちろんデリゲートになります。

さらに、ラムダという記法を用いると、もっと短くなります。

int delegate(int) accum(int a)
{
    return (int b) => a + b;
}

さて、最終の仕上げですが、引数の型も推論してもらいましょう。

int delegate(int) accum(int a)
{
    return b => a + b;
}

おまけとして、accumをもっと短くすると、次のような面白い書き方になります。

enum accum = (int a) => (int b) => a + b;

もっとも短い関数を表すリテラルは{}でしょう。 次いで{;}(){}になります。

void function() f1 = {},
                f2 = {;},
                f3 = (){};

void delegate() d1 = {},
                d2 = {;},
                d3 = (){};

ラムダでもfunctiondelegateの指定ができます。

int delegate(int) accum(int a)
{
    return delegate (int b) => a + b;
}

purenothrow, @safeなどの関数属性は、リテラル表現では推論されますが、次のように指定することも可能です。

int delegate(int) accum(int a)
{
    return delegate int(int b) nothrow @safe { return a + b; };
    return delegate (int b) nothrow @safe { return a + b; };
    return (int b) nothrow @safe { return a + b; };
    return (int b) nothrow @safe => a + b;
    return (b) nothrow @safe => a + b;
}

セーフ関数の中でメモリセーフでない関数や機能を使いたい場合には、@trusted付きのリテラルを使うのが習慣のようです。

int unsafe();           // セーフでない操作

void foo() /*@safe*/    // 関数全体でみるとメモリ安全なのに、unsafeがあるから@safeになれない
{
    //... unsafeの操作がメモリ安全になるような操作

    auto a = unsafe();
    
    //... unsafeの操作がメモリ安全になるような操作
}


void bar() @safe        // メモリ安全でない操作を行ってても、関数全体でみればメモリ安全だからOK
{
    //... unsafeの操作がメモリ安全になるような操作

    auto a = () @trusted => unsafe();

    //... unsafeの操作がメモリ安全になるような操作
}

Goto: 問題8 「関数型スタイルなD」

UFCS(Uniform Function Call Syntax)

関数は通常func(a, b, c)のように呼び出しますが、UFCSという糖衣構文を使うことで、a.func(b, c)というように、funcaのメンバ関数であるかのように記述できます。 たとえば、std.conv.toは、様々な型から他の型への変換を提供しますが、to!string(a)と書くよりも、a.to!string()の方がより英文みたいになってわかりやすくなります。 さらに、f1(f2(f3(a)))と書くよりも、a.f3().f2().f1()と書くほうが、aがどのような順番でどのような処理を受けるかがすぐにわかります。

スライスがレンジとして機能する理由は、UFCSによってstd.arrayの関数がarr.front, arr.popFront(), arr.emptyというように呼び出せるからです。

もちろん、a.f()()はプロパティの記法によって省略できるので、a.f3.f2.f1とも書けます。 素晴らしいですね。

import std.algorithm;
import std.array;
import std.random;
import std.range;
import std.stdio;


void main()
{
    auto gen = Random(unpredictableSeed),   // 乱数生成器を作る
         r = iota(100).randomCover(gen);    // 0 ~ 99までをランダムな順番にする。

    // ランダムに並んだ0 ~ 99のうち、偶数のみを抜き取り(filter!"!(a&1)"), 文字列に変換(map!"a.to!string()")して、それを表示
    writeln(r.filter!"!(a&1)"().map!"a.to!string()"());


    int a = 5;

    // 狂気の如く連ねることも可能
    a.identity.identity.identity.identity.identity.identity.identity.writeln;
}


// そのまま返す関数
auto ref T identity(T)(auto ref T a)
{
    return a;
}

CTFE(Compile Time Function Execution)

関数は、ある程度の条件を満たせばコンパイル時に実行することができます。 コンパイル時とは、そのままの意味で、実行時ではなくてコンパイルしている段階ということです。 C++のテンプレートを用いたテンプレートメタプログラミング(TMP)や、constexprを使用した経験がある人にとっては、D言語のCTFEは素晴らしい機能だとわかるでしょう。 コンパイル時プログラミングの経験がない人にとっては、コンパイル時に関数が走ってなにが嬉しいのだろうと思うでしょう。

もし、定数を事前に(コンパイル時に)計算できるなら? もし、コンパイル時に関数がプログラムを生成してくれたら?

D言語では、CTFE以外にも快適なコンパイル時プログラミングを支援する機能が揃っています。

さて、話はCTFEに戻って、関数がCTFEableであるためには、以下の制約を満たす必要があります。 これらの制約はそのうち緩和される可能性があります。

  • 関数本体がD言語のソースコードとしてある
  • 関数の中で実行する式や文では以下の操作は行えない(実行されない式や文が、以下の操作を行うかもしれなくても、OK)
    • グローバル変数や、ローカルstatic変数の参照
    • インラインアセンブラ(asm文)
    • プラットフォーム依存なキャスト(int[]からfloat[]や、エンディアン依存なキャスト)
    • CTFEableでない関数の呼び出し
    • delete

特別なシンボルとして__ctfeというものがあり、CTFE時にはtrueとなり、実行時にはfalseとなります。

import std.regex;
import std.stdio;


pragma(msg, ctEvaluated());                     // true


/// コンパイル時と、実行時で値が変わる関数。trueならコンパイルに評価された
bool ctEvaluated()
{
    if(!__ctfe){
        int[] arr = new int[10];
        delete arr;                 // コンパイル時には絶対に実行されないのでOK
    }

    return __ctfe;
}


void main()
{
    enum enumValue = ctEvaluated();
    immutable immValue = ctEvaluated();
    const cntValue = ctEvaluated();
    bool mutValue = ctEvaluated();

    static staticValue = ctEvaluated();

    writeln("enum:          ", enumValue);      // true
    writeln("immutable:     ", immValue);       // false
    writeln("const:         ", cntValue);       // false
    writeln("local mutable: ", mutValue);       // false
    writeln("local static:  ", staticValue);    // true
}

問題

解答

  • 問題1 「readIntを実装しよう」

    ユーザーが入力する数字を読み取って、int型で返す関数readIntを書いてください。 readIntの引数や返り値の型は以下のとおりです。

    int readInt();

    ヒント

    • std.conv.to!int
    • std.stdio.readln
    • std.string.chomp
  • 問題2 「sumを実装しよう」

    配列int[]を受け取って、その総和を返す関数sumを書いてください。 sumの引数や戻り値の型は以下のとおりです。

    int sum(int[]);
  • 問題3 「コンパイルできない!」

    次のプログラムをコンパイルしてみると、Deprecation: non-final switch statement without a default is deprecatedというメッセージと共にコンパイルエラー となってしまいます。 エラー文を読んでみると、9行目の普通のswitch文で、defaultが抜けているようです。 idxの値は1, 2, 3しか受け取らないと仮定し、すべての間違いを修正して、コンパイルできるようにしてください。

    import std.stdio;
    
    int g1 = 1,
        g2 = 10,
        g3 = 20;
    
    
    int getGlobalValue(size_t idx)
    {
        switch(idx){
            case 1:
                return g1;
    
            case 2:
                return g2;
    
            case 3:
                return g3;
        }
    }
    
    
    void main()
    {
        writeln(getGlobalValue(1));
        writeln(getGlobalValue(10));
        writeln(getGlobalValue(20));
    }
  • 問題4 「helpメッセージを表示せよ」

    コンソールでコマンドを叩くときに、コマンドライン引数に-hとか--helpを入れると、そのコマンドに対するメッセージがだいたい出力されますよね。 試しにdmd --helpと打ってみると、dmdのコマンド引数の一覧が出力されると思います。(dmdの場合は、dmdだけで表示されるのですが)

    以下に示すプログラムは、add --a=12 --b=13というように呼び出すと12 + 13 = 15と表示されるプログラムです。 また、getopt(...);後の変数h_swには、コマンド引数に-h--helpが出現したかどうかが入っています。 (出現したらtrue)

    このプログラムを少し書き換えて、-h--helpがコマンド引数に現れた場合にはwriteln(appInfo);をして即座にプログラムが終了するようにしてください。

    import std.getopt;
    import std.stdio;
    
    
    immutable appInfo = `example:
    $ add --a=12 --b=13
    a + b = 25
    
    $ add --b=1, --a=3
    a + b = 4
    `;
    
    
    void main(string[] args)
    {
        int a, b;
        bool h_sw;              // argsに-h, --helpが出現したかどうか
    
        getopt(args,
            "a", &a,
            "b", &b,
            "h|help", &h_sw);
    
        writeln("a + b = ", a + b);
    }
  • 問題5 「Grand Total」

    関数を呼び出す毎に過去と今のint型引数の総和を返す関数gtを作ってください。 つまり、次のような関数です。

    writeln(gt(10));            // 10
    writeln(gt(1));             // 11
    writeln(gt(9));             // 20
    writeln(gt(8));             // 28
    
    writeln(gt(5, true));       // 5    第二引数をtrueにすると、0になる
    writeln(gt(10));            // 10
  • 問題6 「Tagged Grand Total」

    Q5とほとんど同じですが、今回の関数は新たにもう一つ引数としてstring型をとります。 この引数stringを「タグ」と呼ぶことにしましょう。 taggedGt関数を作ってもらうわけですが、先ほどのgtは関数gt1つにつき、同時に合計が計算できるのは1つでした。 taggedGtでは、タグを指定することで、同時に複数の合計を計算できるようにしてください。

    writeln(taggedGt("A", 10));             // 10
    writeln(taggedGt("B", 1));              // 1
    writeln(taggedGt("C", 3));              // 3
    
    writeln(taggedGt("A", 100));            // 110
    writeln(taggedGt("B", 10));             // 11
    writeln(taggedGt("C", 3));              // 6
    
    writeln(taggedGt("A", 3, true));        // 3    第3引数がtrueでクリア
    
    writeln(taggedGt("B", 2, true, true));  // 2    第4引数がtrueなら、そのタグの終了
    
    writeln(taggedGt("A", 3));              // 6
    writeln(taggedGt("B", 4));              // 4
    writeln(taggedGt("C", 5));              // 11
    
    taggedGt("A", 0, true, true);           // 数え上げ終わりのときは、必ず第4引数をtrueにする
    taggedGt("B", 0, true, true);           // 同上
    taggedGt("C", 0, true, true);           // 同上

    ヒント:

    • 連想配列
    • 第4引数がtrueのときは、連想配列からそのタグを削除
  • 問題7 「カウンター」

    次のようなソースコードを満たす、createCounterを実装してください。

    auto cnt1 = createCounter();
    
    writeln(cnt1());             // 1
    cnt1();
    cnt1();
    writeln(cnt1());             // 4
    
    auto cnt2 = createCounter();
    
    writeln(cnt2());            // 1
    writeln(cnt1());            // 5
    writeln(cnt2());            // 2
  • 問題8 「関数型スタイルなD」

    関数を関数に渡して処理の内容を変えたりするという技法は、関数型プログラミングというものに属するそうです。 D言語の標準ライブラリPhobosは、基本的にこの技法をベースにして作成されています。

    例えば、std.algorithm.mapを見てみましょう。

    import std.algorithm,
           std.conv,
           std.stdio;
    
    void main(){
        auto r = [0, 1, 2];
    
        writeln(r.map!(a => a + 1)());          // [1, 2, 3]
        writeln(r.map!(a => a.to!string()));    // ["0", "1", "2"]
    }

    map!(a => a + 1)というシンタックスは見慣れませんね。 (まだ説明してないからなのですが。) 簡単に説明すると、map関数にコンパイル時引数として、ラムダa => a + 1を渡しているという意味です。

    r.map!(a => a + 1)()rの全ての要素に1足すという意味で、r.map!(a => a.to!string())rのすべての要素を文字列表現に変換するという意味です。

    filterreduceという素晴らしいものがstd.algorithmにあるのですが、Phobosのドキュメントを読んで、次の関数を作ってください。

    • 配列int[] arrを受け取って、arrの要素のうち、偶数の要素の総和を返す関数sumOfEven
    • 配列int[] arrint needleを受け取って、arrの中で最もneedleに近い値を返す関数getApprxEqElm

    Phobosのドキュメント:

  • 問題募集中

終わりに

実はこの関数の章は、文章量では、他の章に対して3倍(対:式と演算子)~15倍(対:ポインタ)もの量を誇っています。 それほど関数というのは複雑なのです。 ですが、これからは嫌というほど書いていくことになるので、自然と身につくはずです。

さて、次は「メイン関数」について説明します。

キーワード

  • 関数(function)
  • 引数(argument, parameter)
    • 仮引数(parameter)
    • 実引数(argument)
  • 戻り値, 返り値(return value)
  • 関数本体
  • 関数プロトタイプ
  • return
  • assert(0);
  • 仮引数のデフォルト値(parameter's default value)
  • 仮引数の記憶域クラス(parameter storage class)
    • in
    • out
    • ref
    • lazy
    • const
    • immutable
    • shared
    • inout
    • scope
    • (auto ref)
  • 可変個引数関数(variadic function)
  • auto関数, auto ref関数
  • 関数属性(function attribute)
    • @property
    • @safe, @trusted, @system
    • pure
    • nothrow
    • UDA(User Defined Attribute)
    • const, immutable, inout, abstract, final
  • 関数オーバーロード(overload)
    • オーバーロード集合(overload set)
  • ローカルstatic変数(local static variable)
  • ネスト関数(nested function)
  • 関数ポインタ(function pointer)
  • デリゲート(delegate)
  • ラムダ(lambda, λ)
  • UFCS(Uniform Function Call Syntax)
  • CTFE(Compile Time Function Execution)
  • 関数型プログラミング(functional programming)

仕様

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment