D言語/関数

2020年7月20日 (月) 14:32時点におけるAngol Mois (トーク | 投稿記録)による版 (節分け)

関数

関数とは一連の動作ををまとめた手続きであり、値を返すことできます。(C言語の関数についてはC言語/関数

コード例
import std.stdio;

void main() {
    writeln("main内");
    auto n = 21;
    n.foo().writeln;    // writeln(foo(n)); と同じ
    bar();
}

int foo(int n)
{
    writeln("foo内");
    return 2*n;
}

void bar()
{
    writeln("bar内");
}
実行結果
main内
foo内
42
bar内

関数の定義・呼び出し方

スコープ

D言語では、関数の内部における変数の有効範囲の性質が、C言語とはやや違います。なみかっこ { ... } で囲まれた部分は「スコープ」と呼ばれ、スコープは変数やユーザー定義のシンボルが見える範囲の区切りを表します。また、スコープは入れ子にすること可能です。

コード例
import std.stdio;

int a = 99;

void kansu() {
    int a = 2;
    writef("aはkansuでいま%d\n", a);
    // 新たなスコープ
    {
        int a = 42;
         writef("aはkansu内の内側のスコープでいま%d\n", a);
    }
    writef("aはkansu内の内側のスコープでいま%d\n", a);
}

void main()
{
  kansu();  
  writef("aはmainでいま%d\n", a); 
}
実行結果
aはkansuでいま2
aはkansuでいま42
aはkansuでいま2
aはmainでいま99
shadowing

このモジュール自体も一つのスコープであるとみなすことができます。関数を定義する際は、新たなスコープを導入する必要があります。スコープ内で変数を宣言するとき、外側に同じ名前のものがあれば、内部で宣言されたものが優先されます。これを"shadowing"といいます。shadowingした変数はそのスコープでは外側の変数を覆い隠し、外側の変数には何ら影響を及ぼしません。そのため、上のような実行結果になるのです。スコープを抜ければその変数は「消滅」します。

外側のスコープの変数を使用する

スコープの内部で変数をshadowingするまでは、スコープの外側の変数を参照します。(C言語とは、ここらへんの仕組みが違います。)

コード例
import std.stdio;

int a = 99;

void kansu(){
    a = 2; // 変数宣言しない

    writef("aはkansuでいま%d\n", a); 
}

void main()
{
  kansu();  
  writef("aはmainでいま%d\n", a); 
}
実行結果
aはkansuでいま2
aはmainでいま2

上記の例では、関数 kansu では新規宣言されておらず、外部の変数を上書きします。

その結果、aの値がmainでも「2」に変わります。

C言語風に説明するなら「ローカル変数として使いたいなら、ユーザ定義関数内で再度その変数を宣言する必要がある」という事になります。


内部関数

基本

D言語では、関数の内部で関数を定義でき、利用できます。D言語では、ほぼあらゆる言語の要素がいたるスコープで記述でき、例えば、import ですらも特定のスコープ内のみに適用する、といったことが可能です。内部関数は、それのほんの一例です。

コード例
import std.stdio;

void main()
{
    // ↓ これが内部関数
    void naibu() {
	    writeln("inside!");
    }
    // 内部関数はここで終わり

    writeln("1回目");
    naibu();
    
    writeln("2回目");
    naibu();
    
}
表示結果
1回目
inside!
2回目
inside!

標準C言語とC++には、2020年のいまのところ内部関数は無いです。C#には2017年ごろ、C#7にて内部関数が追加されました(なお、C#の内部関数の記法はD言語のそれとは大きく違う)。

内部関数と変数の有効範囲について

内部関数の中にある変数の扱いは、通常のスコープと同様です。

shadowingした場合

コード例
import std.stdio;

int a=7;

void soto() {
    int a=5;
    void naibu() {
        int a=3;
        writef("内部関数の中で aはいま%d\n", a); 
    }
    
    naibu(); // 利用の際は呼び出すのを忘れないように
    writef("関数では aはいま%d\n", a); 
}


void main()
{
  soto();
  writef("mainに帰還。aはいま%d\n", a); 
    
}
表示結果
内部関数の中で aはいま3
関数では aはいま5
mainに帰還。aはいま7

内部関数の置き場所になっているsoto ですら、もはやaの値は(内部関数で定義した3ではなく)7に戻っています。

shadowingしない場合

いっぽう、再宣言しない場合、外側のスコープにある変数を書き換えます。

import std.stdio;

int a=7;

void soto() {
    void naibu() {
	   a=3; // int が無く、再宣言なしの単なる代入命令
	   writef("内部関数の中で aはいま%d\n", a); 
    }
    
    naibu(); // 利用の際は呼び出すのを忘れないように
    writef("関数では aはいま%d\n", a); 
}


void main()
{
  soto();
  writef("mainに帰還。aはいま%d\n", a); 
    
}


表示結果
内部関数の中で aはいま3
関数の隣りでは aはいま3
mainに帰還。aはいま7

3行目のaの値に注目してください。上記コードの内部関数では再宣言しないで代入したので、mainに帰還した際にも、変数aが7でなく3に置き換わっています。


セキュリティ・レベル

C言語に無い特徴として、D言語の「関数」にはセキュリティ・レベルの設定があります。

@system と @trusted と @safe の3種類のセキュリティ・レベルがあります。

何も指定しない場合、レベルは @system になっています。

int foo() @system
{
    return 0;
}

のように指定します。


@system はC言語の関数のように、気楽に使えます。

@safe で宣言された関数では、ポインタの利用が禁止されます。また@safeで宣言された関数は、@safe または @trusted な関数だけしか呼び出しできません。


つまり、@safe の関数は、@system レベルの関数を呼び出しできないのです。

コード例
import std.stdio;

int foo() @system
{
    return 28;
}

void main(){
      
      writeln( foo() );
}
実行結果
28


うごくコード例2
import std.stdio;

int foo2() @safe
{
    return 35;
}

int foo() @safe
{
    return foo2();
}

void main(){
      
      writeln( foo() );
}
実行結果
35


禁止されているコード例
※ エラーになります
import std.stdio;

int foo2() @system{
    return 35;
}

int foo() @safe
{
    return foo2();
}

void main(){
      
      writeln( foo() );
}
実行結果
※ エラーになる。


契約プログラミング

基本

関数を定義する際、入力値の要求事項と、出力値の要求事項とを記述する事ができる。また、自身が記述したアルゴリズムがバグがなく動作した場合に成り立つべき事項を確かめることができる。この仕組みを「契約」と言う。

D言語の契約プログラミングでは、要求事項を見たさない入力または出力がされた際、プログラムの実行を停止する。また、静的な契約 (static assert) も存在し、こちらはコンパイル時間数実行が可能な範囲においてコンパイル時にチェックされ、条件が満たされない場合はコンパイルがエラーとなる。

コード例

下記に、自然数に対して階乗を計算するプログラムを与えます。

import std.stdio;

int factorial( int n )
in {
    assert(n >= 0);    // 階乗への入力は非負整数でなければならない
}
out (result) {
    assert ( result >= n ); // n! ≧ n
    assert ( n <= 3 || result > n^^2 ) // n ≧ 4 のとき、n! > (nの2乗)
}
do {
    if (n == 0) return 1;
    else return n * factorial(n-1);
}

void main(){      
      writeln( factorial(5) );
}
実行結果
120


解説や書式

書式は、関数の冒頭などで。

in {
    assert(入力の要求事項);
} 
out (result) {
    assert(出力の要求事項);
} 
do {
    return 出力内容 ;
}

のように書く。

例として、もし writeln( factorial(4) ); で、引数を4でなく、たとえば「-7」など負の数にすると、入力の要求事項を満たさなくなるので、実行時エラーになる。


なお、「do」はかつてbodyが使われていたが、現在ではbodyは非推奨になっており、最新の仕様ではbodyは廃止である。(まだサポート中のOSにbodyが残っている。)


2020年現在、C言語には、契約プログラミングの機能は無い。

契約プログラミングを公式にサポートしている言語はいまのところ少ない。(非標準のライブラリなどでサポートされている言語はそこそこあるが、しかしプログラム言語の標準ライブラリでサポートされている言語は、かなり少ない。)

コンソール入力との組み合わせの例

readf() などの関数を使うと、キーボードからの入力を受け付けるので、それを使って数値をいろいろと入力して、契約プログラミングのコードをテストしてみよう。

コード例
import std.stdio;

float bbb(float num)
in {
    assert(num >= 0);
} 
out (result) {
    assert(result >= 0);
} 
body {
    return num + 3;
}

void main(){   
    writeln("Please input number");

    int some;
    readf("%d",&some); 


    writef("you input %d\n",some );

    writeln( bbb(some) );
}


実際にテストしてみると、何も入力していない間は、いったんコンパイルできて実行できても、「-4」などマイナスの数を入力すると、そこで実行を停止する。 (コマンド rdmd でも コマンド dmd でも同様。)

なお、契約違反の発見の際、

core.exception.AssertError@hello.d(5): Assertion failure

のようにエラーメッセージが表示される。


もちろん、プラスの「5」など契約に適合した数値を入れているかぎりは、動作する。

契約の使い方に関する注意

契約をユーザーの入力のチェックに使ってはいけない。契約とは、あくまでも自身が書いたアルゴリズム、コードの正しさをチェックするためのみに存在するものである。関数の定義域を制限したい場合は、Assertion Error ではなく例外やエラーメッセージを残すべきである[1]


インターフェース

関数を作る時、インターフェースというものによって、型だけを別個、抜き出して指定できる。

なんのために使うか疑問に思うかもしれないが、おそらく元ネタになっただろうJavaやC#にインターフェースというものがあり、D言語にもインターフェースという機能がある。(詳しくはw:インタフェース (抽象型))


import std.stdio;

interface siyou {
    int kansuu(int n);
}


class jissou : siyou {
    int kansuu(int n) {
        return 4 + n;
    } 
}


void main(){   

    int n = 13;
    
    jissou b = new jissou();
    writeln( b.kansuu(n) );

}


実行結果
17
解説

関数の型だけでは、何も中身が分からないので、クラスの継承によって、その関数の中身を定義する。

C++だと、クラス class と 構造体 struct に互換性があるが、しかしD言語のインターフェースでは class でないといけない。

structで試しても、コンパイルがエラーになるだけである。


備考

契約プログラミングと合わせてインターフェースを使う事もできる[2]

参考文献

  1. ^ Andrei Alexandrescu 著, 長尾 高弘 (翻訳) 『プログラミング言語D』翔泳社 2013年 第一版 10章「契約プログラミング」より
  2. ^ インターフェイス - プログラミング言語 D (日本語訳) 2020年7月19日に閲覧して確認