なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

void型の関数は「値を返さない」のではない

void型とはほとんどの場合において「値がないこと」を示す特別な型である。このとき、関数の戻り値の型としてvoid型を指定したときに「値を返さない」と説明する人をしばしば見かける。
これは解釈次第では間違いではないのだが、「誤解が生じやすい説明」であることを今回は書いていこうと思う。

前置き

今回説明するのは、void型についてである。C、C++C#*1などのvoid *型についてはvoid型と用途が明確に異なるため、説明はしない。

また、コードは基本的にCとする。

void型関数の定義

void型関数は以下のように定義する。

void f(void)
{
    /* 処理内容 */
}

ほかの関数と違うのは、ほとんどの場合においてvoid型の変数」というものは存在しないので変数に結果を代入できないことである*2
しかし値として返すことができないというわけではない。早期returnなどをしたい場合にreturn;を記述することでvoid型の値を返すことができる

void f(void)
{
    if (/* 条件 */) {
        
        /* 処理内容 */
        
        return; // 早期returnしたい場合に左のように書く
    } else {
        
        /* 処理内容 */
        
        return;
    }
}

ただ、void型は先ほども書いた通り「値がないことを示す型」なので、戻り値として返すことはできるものの、値としては存在しない。そのため、()という値を返すユニット型*3の関数とは異なる

void型関数の結果を返すことも可能

JavaPHPC#などではできないが、void型関数の結果を戻り値として返すことも可能である。

/**
 * 結果を戻り値として返される関数
 */
void f(void)
{
    /* 処理内容 */
}

/**
 * void型関数の結果を戻り値として返す関数
 */
void g(void)
{
    /* 処理内容 */
    
    return f();
}

これ自体にはあまりメリットがなさそうなので、JavaPHPC#で関数の結果を戻り値として返すことを禁止するのもわからなくはないが、たとえば以下のようなコードを書くと場合によっては多条件分岐の見栄えが良くなりそうではある(・・・はプログラムの省略を示す)。

// 処理番号
#define PROCESS1 0
#define PROCESS2 1
#define PROCESS3 2
 ・
 ・
 ・

// 処理
void process1(/* 引数 */)
{
    /* 処理内容 */
}
void process2(/* 引数 */)
{
    /* 処理内容 */
}
void process3(/* 引数 */)
{
    /* 処理内容 */
}
 ・
 ・
 ・

/**
 * 処理を実行する関数
 */
void run_process(int proc_num)
{
    switch (proc_num) {
    case PROCESS1:
        return process1(/* パラメータ */);
    case PROCESS2:
        return process2(/* パラメータ */);
    case PROCESS3:
        return process3(/* パラメータ */);
     ・
     ・
     ・
    default:
        return; // 何もしない
    }
}

そもそも「値を返さない関数」はvoid型でなくても作れる

そもそも論だが、「値を返さない関数」はvoid型じゃなくても作れる。無理だろうと思われるかもしれないが、以下のコードを見てほしい。

int f(void)
{
    // ここで無限ループする(つまり、"値を返さない")
    while (1) {
        
        /* 処理内容(break/returnなし) */
        
    }
    
    return 0;
}

この関数はint型の関数でこそあるものの、無限ループのために値を返せない、つまり、「値を返さない関数」と説明できる関数になっているのである。

ここから何が言いたいのかと言うと、void型の関数を「値を返さない関数」と説明すると、int型の関数も場合によってはそうだと説明できるため、矛盾するし、そういう意味じゃないにしても誤解が生じやすいことは確かだ。

「値を返さない関数」には正体がある

実は「値を返さない関数」は型理論的には、⊥型*4の関数の特徴である。この⊥型の関数こそが「値を返さない関数」としての役割を果たしており、これによってプログラムは無限ループや例外処理などを実現することが可能となるのである。

しかしちょっと待ってほしい。⊥型がそうなのであればint型やvoid型などの関数はなぜ「値を返さない関数」として定義できるのか?と多くの方が疑問を持ったことだろう。

実は型理論的には、⊥型はint型やvoid型などすべての型の部分型である。つまり、ほとんどのプログラミング言語では⊥型を含むすべての型が⊥型の特徴を持っているのである。

したがって、「値を返さない関数」はvoid型の関数としての特徴ではなく⊥型の関数としての特徴であり、「void型の関数は『値を返さない関数』である」という説明はあまり適切でないことがわかる。

おまけ:「void」と名乗りながら実はvoid型でない型

ここからは蛇足であるが、一つ注意点がある。

SwiftのVoid型は()(空のタプル)型の型エイリアス、つまり、ユニット型である。そのため、SwiftのVoid型は実はvoid型ではないため、Swiftプログラマは注意する必要がある*5

参考

*1:C#の場合はunsafeコードを書くとき限定

*2:TypeScriptやPHPなどは代入することが可能である。ただ、TypeScriptやPHPなどの場合、代入されるのはundefinedやNULLであり、「void型特有の値」として代入するわけではない

*3:void型と同様に何もないことを示すための型である、が、実際には"()"という唯一の値を持つため、値がないわけではない

*4:ボトム型とも呼ばれる

*5:まぁ、あまり意識する必要はないと思うが・・・