関数パラメータ

この章では、さまざまな種類の関数パラメータについて説明する。

この章の概念の一部は、この本の前半で既に紹介している。例えば、foreachループの章で見たrefキーワードは、foreachループで、要素のコピーではなく実際の要素を使用できるようにするものだった。

さらに、constおよびimmutableキーワード、値型と参照型の違いについても、前の章で説明した。

パラメータを利用して結果を生成する関数を作成した。例えば、次の関数は、そのパラメータを計算に使用している。

double weightedAverage(double quizGrade, double finalGrade) {
    return quizGrade * 0.4 + finalGrade * 0.6;
}
D

この関数は、小テストの成績の40%と期末試験の成績の60%を平均して、平均成績を計算する。この関数の使用方法は次の通りだ。

int quizGrade = 76;
int finalGrade = 80;

writefln("Weighted average: %2.0f",
         weightedAverage(quizGrade, finalGrade));
D
パラメーターは常にコピーされる

上記のコードでは、2つの変数がweightedAverage()の引数として渡されている。この関数は、そのパラメータを使用している。この事実から、この関数は引数として渡された実際の変数を使用しているとの誤解を招くかもしれない。実際には、この関数が使用しているのは、それらの変数のコピーである

この区別は、パラメータを変更するとそのコピーのみが変更されるため、重要である。これは、パラメータを変更しようとしている(つまり、副作用を生じさせようとしている)次の関数で確認できる。ゲームキャラクターのエネルギーを減らすために、次の関数が記述されていると仮定しよう。

void reduceEnergy(double energy) {
    energy /= 4;
}
D

reduceEnergy()をテストするプログラムは以下の通りだ:

import std.stdio;

void reduceEnergy(double energy) {
    energy /= 4;
}

void main() {
    double energy = 100;

    reduceEnergy(energy);
    writeln("New energy: ", energy);
}
D
function_parameters.1

出力:

新しいエネルギー: 100     ← 変更なし

reduceEnergy()は、そのパラメータの値を元の値の4分の1に減少させるが、main()内の変数energyは変化しない。これは、main()内の変数energyreduceEnergy()のパラメータenergyは別物であり、パラメータはmain()内の変数のコピーであるためだ。

これをより詳しく確認するために、writeln()式をいくつか挿入しよう。

import std.stdio;

void reduceEnergy(double energy) {
    writeln("Entered the function      : ", energy);
    energy /= 4;
    writeln("Leaving the function      : ", energy);
}

void main() {
    double energy = 100;

    writeln("Calling the function      : ", energy);
    reduceEnergy(energy);
    writeln("Returned from the function: ", energy);
}
D
function_parameters.2

出力:

関数を呼び出す      : 100
関数に入った      : 100
関数を離れる      : 25   ← パラメータが変更された、
関数から戻った: 100  ← 変数は変わらない
参照されている変数はコピーされない

スライス、連想配列、クラス変数などの参照型のパラメータも関数にコピーされる。ただし、参照されている元の変数(スライスや連想配列の要素、クラスオブジェクトなど)はコピーされない。事実上、このような変数は参照として関数に渡される。つまり、パラメータは元のオブジェクトへの別の参照になる。これは、参照を通じて変更を行った場合、元のオブジェクトも変更されることを意味する。

文字のスライスであるため、これは文字列にも適用される:

import std.stdio;

void makeFirstLetterDot(dchar[] str) {
    str[0] = '.';
}

void main() {
    dchar[] str = "abc"d.dup;
    makeFirstLetterDot(str);
    writeln(str);
}
D
function_parameters.3

パラメーターの最初の要素に変更を加えると、main()内の実際の要素にも影響する:

.bc

ただし、元の切り出しと連想配列の変数はコピーで渡される。パラメーター自体がrefとして修飾されていない場合、予期しない結果が生じる可能性がある。

スライスの意外な参照セマンティクス

スライスとその他の配列機能の章で見たように、スライスに要素を追加すると要素の共有が終了する場合がある。当然、共有が終了すると、上記のstrのようなスライスパラメーターは、渡された元の変数の要素への参照ではなくなる。

例えば、次の関数によって追加された要素は、呼び出し元からは見えない。

import std.stdio;

void appendZero(int[] arr) {
    arr ~= 0;
    writefln("Inside appendZero()       : %s", arr);
}

void main() {
    auto arr = [ 1, 2 ];
    appendZero(arr);
    writefln("After appendZero() returns: %s", arr);
}
D
function_parameters.4

この要素は、関数パラメータにのみ追加され、元のスライスには追加されない。

appendZero()の内部       : [1, 2, 0]
appendZero()が返った後: [1, 2]    ← No 0

新しい要素を元の切り出しに追加する必要がある場合は、切り出しをrefとして渡す必要がある:

void appendZero(ref int[] arr) {
    // ...
}
D

ref修飾子は以下で説明する。

連想配列の意外な参照セマンティクス

関数パラメータとして渡される連想配列も、連想配列はnullとして、空の状態ではなく開始するため、予期しない動作をする可能性がある。

この文脈では、nullは初期化されていない連想配列を意味する。連想配列は、最初のキーと値のペアが追加されると自動的に初期化される。その結果、関数がnullの連想配列に要素を追加した場合、その要素は元の変数では見ることができない。これは、パラメータは初期化されるものの、元の変数はnullのままになるためだ。

import std.stdio;

void appendElement(int[string] aa) {
    aa["red"] = 100;
    writefln("Inside appendElement()       : %s", aa);
}

void main() {
    int[string] aa;    // ← 最初はnull
    appendElement(aa);
    writefln("After appendElement() returns: %s", aa);
}
D
function_parameters.5

元の変数には追加された要素は存在しない:

appendElement()の内部       : ["red":100]
appendElement()が返った後: []    ← まだnull

一方、連想配列が最初にnullでなかった場合、追加された要素は呼び出し元からも参照できる:

int[string] aa;
aa["blue"] = 10;  // ← 呼び出し前はnullではない
appendElement(aa);
D

この場合、追加された要素は呼び出し元からも見える:

appendElement()の内部["red":100, "blue":10]
appendElement()が返った後["red":100, "blue":10]

そのため、連想配列をrefパラメーターとして渡す方が良いかもしれない。これについては後で説明する。

パラメーター修飾子

パラメータは、上記の一般的な規則に従って関数に渡される。

これらは、パラメーター定義に修飾子が指定されていない場合のデフォルトのルールだ。以下の修飾子は、パラメーターの渡し方や、それらに対して許可される操作を変更する。

in

デフォルトでは、inパラメーターはconstパラメーターと同じだ。これらは変更できない:

void foo(in int value) {
    value = 1;    // ← コンパイルエラー
}
D

ただし、‑preview=inコンパイラコマンドラインスイッチを使用すると、inパラメータは、"このパラメータはこの関数によってのみ入力として使用される"という意図を表現するのに、より有用になる。

dmd -preview=in deneme.d
Bash

-preview=in inパラメーターの意味を変更し、inパラメーターに引数を渡す際にコンパイラーがより適切なメソッドを選択できるようにする:

‑preview=inコマンドラインスイッチの使用有無に関わらず、constパラメーターよりもinパラメーターを使用することをおすすめする。

out

関数は、生成した結果を戻り値として返すことはご存じの通りだ。戻り値が1つしかないことは、1つの関数で複数の結果を生成する必要がある場合、制限となることがある。(注釈:戻り値の型をTupleまたはstructと定義することで、複数の結果を返すことができる。これらの機能については、後の章で説明する。)

outキーワードを使用すると、関数はパラメータを通じて結果を返すことができる。outパラメータが関数内で変更されると、その変更は関数に渡された元の変数にも反映される。ある意味で、割り当てられた値はoutパラメータを通じて関数から出力される

2つの数値を割り、商と余りを両方生成する関数を見てみよう。戻り値は商に使用され、余りはoutパラメータを通じて返される

import std.stdio;

int divide(int dividend, int divisor, out int remainder) {
    remainder = dividend % divisor;
    return dividend / divisor;
}

void main() {
    int remainder;
    int result = divide(7, 3, remainder);

    writeln("result: ", result, ", remainder: ", remainder);
}
D
function_parameters.6

関数のremainderパラメータを変更すると、main()remainder変数が変更される(名前は同じである必要はない)。

結果余り
21

呼び出しサイトでの値に関係なく、outパラメータは、まずその型の.init値に自動的に割り当てられる。

import std.stdio;

void foo(out int parameter) {
    writeln("After entering the function      : ", parameter);
}

void main() {
    int variable = 100;

    writeln("Before calling the function      : ", variable);
    foo(variable);
    writeln("After returning from the function: ", variable);
}
D
function_parameters.7

関数内でパラメータに明示的な代入がない場合でも、パラメータの値は自動的にintの初期値となり、main()内の変数に影響を与える。

関数呼び出し前      : 100
関数に入った後      : 0  ← int.initの値
関数から戻った後: 0

この例からわかるように、outパラメータは関数に値を渡すことはできない。これらは、関数から値を渡すためだけに使用される。

後の章で、outパラメータの代わりにTupleまたはstruct型を返すほうが良いことを説明する。

const

constパラメーターよりもinパラメーターを使用することをおすすめする。

前述のように、constは、関数内でパラメータが変更されないことを保証する。プログラマにとっては、特定の変数が関数によって変更されないことを知っておくと便利だ。constは、constimmutable、および変更可能な変数をそのパラメータとして渡すことを可能にするため、関数の有用性を高める。

import std.stdio;

dchar lastLetter(const dchar[] str) {
    return str[$ - 1];
}

void main() {
    writeln(lastLetter("constant"));
}
D
function_parameters.8
immutable

前述のように、immutableは、関数に対して特定の変数を不変にすることを要求する。この要件により、次の関数は、immutable要素(文字列リテラルなど)を含む文字列でのみ呼び出すことができる。

import std.stdio;

dchar[] mix(immutable dchar[] first,
            immutable dchar[] second) {
    dchar[] result;
    int i;

    for (i = 0; (i < first.length) && (i < second.length); ++i) {
        result ~= first[i];
        result ~= second[i];
    }

    result ~= first[i..$];
    result ~= second[i..$];

    return result;
}

void main() {
    writeln(mix("HELLO", "world"));
}
D
function_parameters.9

immutableはパラメータに要件を強制するため、不変性が要求される場合にのみ使用すべきだ。それ以外の場合は、immutableconst、および変更可能な変数を受け入れるconstの方が一般的に有用だ。

ref

このキーワードを使用すると、通常はコピー(つまり値)として渡される変数を、参照として渡すことができる。

r値(次の章を参照)は、refパラメータとして関数に渡すことはできない。

先ほど見た、元の変数を変更するreduceEnergy()関数では、そのパラメータをrefとして受け取る必要がある。

import std.stdio;

void reduceEnergy(ref double energy) {
    energy /= 4;
}

void main() {
    double energy = 100;

    reduceEnergy(energy);
    writeln("New energy: ", energy);
}
D
function_parameters.10

この場合、パラメータに対する変更は、main()で元の変数にも反映される:

新しいエネルギー25

このように、refパラメータは入力と出力の両方として使用できる。refパラメータは、元の変数の別名とも考えることができる。上記の関数パラメータenergyは、main()内の変数energyの別名だ。

outパラメータと同様に、refパラメータも関数に副作用を持たせることができる。実際、reduceEnergy()は値を返しさない。単一のパラメータを通じて副作用を引き起こすだけである。

関数型プログラミングと呼ばれるプログラミングスタイルでは、副作用よりも戻り値が優先されるため、一部の関数型プログラミング言語では副作用がまったく使用できない。これは、戻り値によってのみ結果を生成する関数は、理解、実装、および保守が容易であるためだ。

同じ関数は、副作用を引き起こす代わりに結果を返すことで、関数型プログラミングスタイルで記述することができる。プログラムの変更部分は強調表示されている。

import std.stdio;

double reducedEnergy(double energy) {
    return energy / 4;
}

void main() {
    double energy = 100;

    energy = reducedEnergy(energy);
    writeln("New energy: ", energy);
}
D
function_parameters.11

関数の名前の変更にも注意。動詞ではなく、名詞になった。

auto ref

この修飾子はテンプレートでのみ使用できる。次の章で見るように、auto refパラメータはl値を参照で、r値をコピーで受け取る。

inout

名前が"in"と"out"から成っているにもかかわらず、このキーワードは入力と出力を意味しない。入力と出力は、refキーワードで実現されることは既に説明した。

inoutは、パラメータの変更可能性を戻り値の型に伝える。パラメータがconstimmutable、またはmutableの場合、戻り値もそれぞれconstimmutable、またはmutableになる。

inoutがプログラムでどのように役立つかを確認するために、パラメータの内部要素にスライスを返す関数を見てみよう。

import std.stdio;

int[] inner(int[] slice) {
    if (slice.length) {
        --slice.length;               // 末尾から切り取る

        if (slice.length) {
            slice = slice[1 .. $];    // 先頭から切り取る
        }
    }

    return slice;
}

void main() {
    int[] numbers = [ 5, 6, 7, 8, 9 ];
    writeln(inner(numbers));
}
D
function_parameters.12

出力:

[6, 7, 8]

この本でこれまで説明してきたことに従って、この関数をより有用なものにするためには、そのパラメータはconst(int)[]であるべきだ。これは、関数内で要素が変更されないためだ。(パラメータのスライス自体は元の変数のコピーなので、それを変更しても問題はないことに注意しよう。)

しかし、そのように関数を定義すると、コンパイルエラーが発生する。

int[] inner(const(int)[] slice) {
    // ...
    return slice;    // ← コンパイルエラー
}
D

このコンパイルエラーは、const(int)のスライスは、変更可能な intのスライスとして返すことができないことを示している。

エラー: 型const(int)[]の式(スライス)を
int[]に暗黙的に変換できない
Undefined

戻り値の型をconst(int)[]と指定すれば解決すると思われるかもしれない。

const(int)[] inner(const(int)[] slice) {
    // ...
    return slice;    // 現在コンパイルできる
}
D

これでコードはコンパイルされるようになったが、制限がある。関数が変更可能な要素のスライスで呼び出された場合でも、返されるスライスはconst要素で構成されることになる。この制限がどれほど厳しいかを確認するために、スライスの内部要素を変更しようとする次のコードを見てみよう。

int[] numbers = [ 5, 6, 7, 8, 9 ];
int[] middle = inner(numbers);    // ← コンパイルエラー
middle[] *= 10;
D

const(int)[]型の返されたスライスは、int[]型のスライスに代入できないため、エラーになる。

エラー: 型const(int)[]の式(inner(numbers))を
int[]に暗黙的に変換できない
Undefined

しかし、変更可能な要素のスライスから始めたため、この制限は人為的で不運なものとなっている。inoutは、パラメータと戻り値の間のこの変更可能性の問題を解決する。これは、パラメータと戻り値の両方の型で指定され、前者の変更可能性を後者に引き継ぐ。

inout(int)[] inner(inout(int)[] slice) {
    // ...
    return slice;
}
D

この変更により、同じ関数をconstimmutable、および変更可能なスライスで呼び出すことができるようになった。

{
    int[] numbers = [ 5, 6, 7, 8, 9 ];
    // 戻り値の型は、変更可能な要素のスライスである。
    int[] middle = inner(numbers);
    middle[] *= 10;
    writeln(middle);
}
{
    immutable int[] numbers = [ 10, 11, 12 ];
    // 戻り値の型は、変更不可能な要素のスライスである。
    immutable int[] middle = inner(numbers);
    writeln(middle);
}
{
    const int[] numbers = [ 13, 14, 15, 16 ];
    // 戻り値の型は、const要素のスライスである。
    const int[] middle = inner(numbers);
    writeln(middle);
}
D
lazy

引数を使用する関数に入る前に、引数が評価されることは当然のことだ。例えば、以下の関数add()は、他の2つの関数の戻り値で呼び出される。

result = add(anAmount(), anotherAmount());
D

add()が呼び出されるためには、まずanAmount()anotherAmount()が呼び出されなければならない。そうしないと、add()が必要とする値が利用できなくなってしまう。

関数を呼び出す前に引数を評価することを、熱心な評価と呼ぶ。

しかし、特定の条件によっては、一部のパラメータが関数内でまったく使用されない場合がある。そのような場合、引数を熱心に評価することは無駄になる。

この状況の典型的な例は、メッセージの重要度が特定の設定値以上である場合にのみメッセージを出力するロギング関数だ。

enum Level { low, medium, high }

void log(Level level, string message) {
    if (level >= interestedLevel) {
        writeln(message);
    }
}
D

例えば、ユーザーがLevel.highのメッセージのみに関心がある場合、Level.mediumのメッセージは出力されない。しかし、関数を呼び出す前に引数は評価される。例えば、以下のformat()式は、それ自体が呼び出すgetConnectionState()呼び出しも含めて、メッセージが決して出力されない場合、すべて無駄になってしまう。

if (failedToConnect) {
    log(Level.medium,
        format("Failure. The connection state is '%s'.",
               getConnectionState()));
}
D

lazyキーワードは、パラメータとして渡された式は、必要な場合にのみ評価されることを指定する。

void log(Level level, lazy string message) {
   // ... 関数の本体は以前と同じ ...
}
D

この場合、messageパラメータが使用された場合にのみ、式が評価される。

注意すべき点は、lazyパラメータは、その関数内でそのパラメータが使用されるたびに評価されることだ。

例えば、次の関数のlazyパラメータは関数内で3回使用されているため、その値を提供する式は3回評価される。

import std.stdio;

int valueOfArgument() {
    writeln("Calculating...");
    return 1;
}

void functionWithLazyParameter(lazy int value) {
    int result = value + value + value;
    writeln(result);
}

void main() {
    functionWithLazyParameter(valueOfArgument());
}
D
function_parameters.13

出力:

計算中...
計算中...
計算中...
3
scope

このキーワードは、パラメータが関数のスコープ外では使用されないことを指定している。この記事の執筆時点では、scopeは、関数が@safeと定義され、-dip1000 コンパイラスイッチが使用されている場合にのみ有効である。DIPはD改善提案の略だ。DIP1000はこの記事の執筆時点では実験的な機能であるため、すべての場合で期待どおりに動作するとは限らない。

dmd -dip1000 deneme.d
Bash
int[] globalSlice;

@safe int[] foo(scope int[] parameter) {
    globalSlice = parameter;    // ← コンパイルエラー
    return parameter;           // ← コンパイルエラー
}

void main() {
    int[] slice = [ 10, 20 ];
    int[] result = foo(slice);
}
D
function_parameters.14

上記の関数は、2つの点でscopeの約束に違反している。パラメータをグローバル変数に代入し、それを返している。これらの動作により、関数の終了後もパラメータにアクセス可能になってしまう。

shared

このキーワードは、パラメーターが実行スレッド間で共有可能であることを要求する:

void foo(shared int[] i) {
    // ...
}

void main() {
    int[] numbers = [ 10, 20 ];
    foo(numbers);    // ← コンパイルエラー
}
D
function_parameters.15

上記のプログラムは、引数がsharedでないためコンパイルできない。コンパイル可能にするための必要な変更は以下の通りだ:

shared int[] numbers = [ 10, 20 ];
foo(numbers);    // 現在コンパイルできる
D

sharedキーワードについては、後述のデータ共有と並行処理の章で説明する。

return

関数が、そのrefパラメータの1つを直接返すことが便利な場合がある。例えば、次のpick()関数は、そのパラメータの1つをランダムに選択して返し、呼び出し元が幸運なパラメータを直接変更できるようにする。

import std.stdio;
import std.random;

ref int pick(ref int lhs, ref int rhs) {
    return uniform(0, 2) ? lhs : rhs;    // ← コンパイルエラー
}

void main() {
    int a;
    int b;

    pick(a, b) = 42;

    writefln("a: %s, b: %s", a, b);
}
D
function_parameters.16

その結果、main()内のaまたはbのいずれかに、42の値が割り当てられる。

ab
420
ab
042

残念ながら、pick()の引数の1つは、返される参照よりも寿命が短い場合がある。例えば、次のfoo()関数は、2つのローカル変数を使用してpick()を呼び出し、事実上、そのうちの1つへの参照を返す。

import std.random;

ref int pick(ref int lhs, ref int rhs) {
    return uniform(0, 2) ? lhs : rhs;    // ← コンパイルエラー
}

ref int foo() {
    int a;
    int b;

    return pick(a, b);    // ← バグ: 無効な参照を返している
}

void main() {
    foo() = 42;           // ← バグ: 無効なメモリに書き込んでいる
}
D
function_parameters.17

abの両方の有効期間はfoo()を離れると終了するため、main()での代入は有効な変数に対して行うことができない。その結果、未定義の動作が発生する。

未定義の動作とは、プログラミング言語の仕様ではプログラムの動作が定義されていない状況を指す。未定義の動作を含むプログラムの動作については、何も言うことはできない。(ただし、実際には、上記のプログラムでは、値42は、aまたはbのいずれかが占めていたメモリ位置に書き込まれる可能性が高く、そのメモリ位置は現在、無関係な変数の一部となっている可能性があり、その結果、その無関係な変数の値が破損する可能性がある。

returnキーワードをパラメーターに適用することで、このようなバグを防止できる。これは、パラメーターが返される参照よりも長いライフタイムを持つ変数への参照でなければならないことを指定する:

import std.random;

ref int pick(return ref int lhs, return ref int rhs) {
    return uniform(0, 2) ? lhs : rhs;
}

ref int foo() {
    int a;
    int b;

    return pick(a, b);    // ← コンパイルエラー
}

void main() {
    foo() = 42;
}
D
function_parameters.18

この場合、コンパイラは、pick()の引数の寿命が、foo()が返そうとしている参照の寿命よりも短いことを認識する。

エラー: ローカル変数aへの参照のエスケープ
エラー: ローカル変数bへの参照のエスケープ
Undefined

この機能は"密封された参照"と呼ばれる。

注釈:コンパイラがreturnキーワードがなくてもpick()を検査してバグを検出することは考えられるが、一部の関数の本体は、すべてのコンパイルでコンパイラが利用できない場合があるため、通常はそうはいかない。

要約
演習

次のプログラムは、2つの引数の値を交換しようとしている。

import std.stdio;

void swap(int first, int second) {
    int temp = first;
    first = second;
    second = temp;
}

void main() {
    int a = 1;
    int b = 2;

    swap(a, b);

    writeln(a, ' ', b);
}
D
function_parameters.19

しかし、このプログラムはabには何の影響も与えていない:

1 2          ← 入れ替わっていない

abの値が入れ替わるように、関数を修正しよう。