
アドレス渡しやグローバル変数を行き当たりばったりで適当に使っていると、プログラムが大きくなってくると、あとから見た時に読みずらかったり、こんがらがったコードになりがちです。
なるべく読みやすいコード、部品として使いまわしやすい関数を作っていくために、 可能な範囲で純粋関数、イミュータブル(immutable)な引数の関数、副作用のない関数を目指して作っていきましょう!
このページではそれぞれについて例を交えながら説明していきます。
副作用のある関数とはどのようなものでしょうか。 副作用は英語で side effect と書きます。
単純な関数であれば、与えられた引数を元に、関数で処理して return で値を返しますが、 それ以外の影響を副作用と呼びます。
下のような、 return する以外の影響は副作用です。
例えばゲーム作りでは、ゲームのデータを保存する関数などは副作用があると言えます。 また、ゲームロードしたときに、グローバル領域に確保したメモリ上にデータを読み込むことも 副作用がると言えます。
下はレベルアップ関数が副作用を持つ例です。
#include <stdio.h>
static int level = 1; // レベルの変数
int levelup() {
level++; // 副作用あり
return level;
}
int main(){
printf("レベルアップしてレベルは %d になりました。\n",levelup());
printf("レベルアップしてレベルは %d になりました。\n",levelup());
printf("レベルアップしてレベルは %d になりました。\n",levelup());
}
実行結果は下になります。
レベルアップしてレベルは 2 になりました。
レベルアップしてレベルは 3 になりました。
レベルアップしてレベルは 4 になりました。
副作用があることは悪いことではなく、作るプログラムに求める機能によっては必ず発生します。
副作用がある関数を作ったり、使うときには、 どのような副作用が起こるのかを把握して置く必要があります。
例えばゲームデータをスロット1に保存する関数(save_slot1)を考えます。
int isSuccess = save_slot1( gamedata );
この save_slot1 関数は外部にデータを保存し、他のセーブデータを確認する関数に 影響します。例えば、セーブデータのスロット1状態を確認する関数(check_slot1)があった場合、 save_slot1 で保存するまでは、 check_slot1 の結果は「無し」だったのに、 save_slot1 で保存したら、check_slot1 の結果が「有り」となります。
関数の中だけで処理が完結しておらず、なにかしら関数の外に影響があるため、 「副作用あり」の関数は、どのような副作用があるかを知り、取扱いに注意する必要があります。
※今後にお話する、C言語のプログラムのテストは、副作用がない純粋関数の方が 実施しやすいです。
イミュータブル(immutable)とは、日本語では「不変」という意味です。
イミュータブルは一度つくったら後から値を変更できない状態の変数や機能などを指します。
例えば、C言語で不変の数値(つまり定数)を定義する場合は const というキーワードを付けてあげます。
int main(){
const int startLevel= 1;
}
2 行目で int 型の定数 startLevel を宣言し、値として 1 を定めています。
これで、後から startLevel = 2;
などと書いて値を変更しようとしても
コンパイルする時に、コンパイラがエラーを出してくれます。
扱う変数をちゃんとイミュータブルであると宣言するとどんなメリットがあるでしょうか。
関数へアドレス渡しをすると、 渡した先の関数の中で、渡すときにセットした引数のメモリアドレスが指す中身が変更されてしまうことがあります。
便利なのですが、ふとした時にバグとなる場合があります。
例:最初のレベル・体力とレベルアップのレベル・体力を表示する例
#include <stdio.h>
int getNextLevelHitpoint(int *lv, int *hp) {
*lv = *lv + 1;
return (*lv * 4 ) + *hp;
}
int main(){
int level =1;
int firstHitpoint = 10;
int nextHitpoint = getNextLevelHitpoint(&level, &firstHitpoint);
printf("レベル %d, HP = %d\n", level, firstHitpoint);
printf("レベル %d, HP = %d\n", level, nextHitpoint);
}
以下のような結果を期待します。
ところが、これは予期せず下のような結果になってしまいました。
レベル 2, HP = 10
レベル 2, HP = 18
レベル 1 の時の結果が表示されておらず、いきなり 2 から始まっています。
これは、getNextLevelHitpoint 関数のなかで、渡された レベルを表す変数 level のアドレスの中身を直接書き換えてしまっているからです。
具体的には 4 行目で、次のレベルの計算をするときに アドレス渡しされた level の変数の格納されているメモリアドレスの中身を + 1 してしまっています。
渡されたメモリアドレスの中身はアドレスするだけとし、更新をしないようにコーディングすることとし、 引数の中身を間違って変更しないように型修飾子である const を付けましょう。
下は変更後のイメージです。
#include <stdio.h>
int getNextLevelHitpoint(const int *lv, const int *hp) {
int nextLevel = *lv + 1;
return (nextLevel * 4 ) + *hp;
}
int main(){
int level =1;
int firstHitpoint = 10;
int nextHitpoint = getNextLevelHitpoint(&level, &firstHitpoint);
printf("lvl %d, hp = %d\n", level, firstHitpoint);
printf("lvl %d, hp = %d\n", level, nextHitpoint);
}
4 行目が変更されており、*lv 本体を変更せずに、 関数の中でのみ使う一次的な変数 nextLevel に値をコピーして、計算をしています。
getNextLevelHitpoint 関数に渡した level や firstHitpoint などの変数は 一切変更されておらず、イミュータブルな引数をもつ関数になりました。
でもでも、今回の例であれば、そもそもgetNextLevelHitpoint関数にポインタを渡す必要はありませんので、 アドレス渡しをせずに下の用に書いてあげるのがよいですね。
#include <stdio.h>
int getNextLevelHitpoint(const int lv, const int hp) {
int nextLevel = lv + 1;
return (nextLevel * 4 ) + hp;
}
int main(){
int level =1;
int firstHitpoint = 10;
int nextHitpoint = getNextLevelHitpoint(level, firstHitpoint);
printf("lvl %d, hp = %d\n", level, firstHitpoint);
printf("lvl %d, hp = %d\n", level, nextHitpoint);
}
イミュータブルでないほうが良いケース 画像処理やマップデータを操作するなど、でっかいデータを関数に渡して、一部のデータを変更する関数などの 引数をイミュータブルにすると、毎回関数に渡してデータを操作するときに、関数呼び出し元のデータのコピーを作って、 関数の中でコピーした内容を編集して結果を返すということをしなければならず、 その手間からプログラムを実行する処理が遅くなってしまう可能性があります。
純粋関数ってどんな関数でしょうか。下のような特徴があります。
例えば下のような関数(kick)は純粋関数です!
#include <stdio.h>
int kick( power ) {
return power * 3;
}
int main(){
printf("キックで %d のダメージ!\n", kick(5) );
printf("キックで %d のダメージ!\n", kick(4) );
}
純粋関数は副作用を持ってないので、使う側は引数と、return で返ってくる結果の 仕様を分かっていれば良いのでとても使いやすいです。
なるべく副作用を減らして、純粋関数で実装できるものは 純粋関数にしていきたいですね。
http://www.cs.tsukuba.ac.jp/~kam/lecture/plm2011/5-web.pdf
更新日 | 更新内容 |
---|---|
更新なし |
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: