Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年2月10日に更新

第2章41 副作用がある関数、純粋関数、イミュータブル

イチからゲーム作りで覚えるC言語
第2章40 swap関数を作ってポインタ変数の中身を交換する : PREV
NEXT : 第2章42 ポインタへのポインタ :

Summury

アドレス渡しやグローバル変数を行き当たりばったりで適当に使っていると、プログラムが大きくなってくると、あとから見た時に読みずらかったり、こんがらがったコードになりがちです。

なるべく読みやすいコード、部品として使いまわしやすい関数を作っていくために、 可能な範囲で純粋関数イミュータブル(immutable)な引数の関数、副作用のない関数を目指して作っていきましょう!

このページではそれぞれについて例を交えながら説明していきます。

副作用のある関数とは

副作用のある関数とはどのようなものでしょうか。 副作用は英語で side effect と書きます。

単純な関数であれば、与えられた引数を元に、関数で処理して return で値を返しますが、 それ以外の影響を副作用と呼びます。

下のような、 return する以外の影響は副作用です。

  • 関数の中でファイルを補助記憶装置(SSD や HDD、USB)に書き出し
  • 引数から渡されたポインタの先の値を書き換え
  • 静的な変数の中身を書き換え

例えばゲーム作りでは、ゲームのデータを保存する関数などは副作用があると言えます。 また、ゲームロードしたときに、グローバル領域に確保したメモリ上にデータを読み込むことも 副作用がると言えます。

下はレベルアップ関数が副作用を持つ例です。

 
no_const.c
#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());
}

実行結果は下になります。

no_const結果
レベルアップしてレベルは 2 になりました。
レベルアップしてレベルは 3 になりました。
レベルアップしてレベルは 4 になりました。

副作用があることは悪いことではなく、作るプログラムに求める機能によっては必ず発生します。

副作用がある関数への注意

副作用がある関数を作ったり、使うときには、 どのような副作用が起こるのかを把握して置く必要があります。

例えばゲームデータをスロット1に保存する関数(save_slot1)を考えます。

 int isSuccess = save_slot1( gamedata );

この saveslot1 関数は外部にデータを保存し、他のセーブデータを確認する関数に 影響します。例えば、セーブデータのスロット1状態を確認する関数(checkslot1)があった場合、 saveslot1 で保存するまでは、 checkslot1 の結果は「無し」だったのに、 saveslot1 で保存したら、checkslot1 の結果が「有り」となります。

関数の中だけで処理が完結しておらず、なにかしら関数の外に影響があるため、 「副作用あり」の関数は、どのような副作用があるかを知り、取扱いに注意する必要があります。

※今後にお話する、C言語のプログラムのテストは、副作用がない純粋関数の方が 実施しやすいです。

イミュータブルってなに?

イミュータブル(immutable)とは、日本語では「不変」という意味です。

イミュータブルは一度つくったら後から値を変更できない状態の変数や機能などを指します。

例えば、C言語で不変の数値(つまり定数)を定義する場合は const というキーワードを付けてあげます。

 
const
int main(){
  const int startLevel= 1;
}

2 行目で int 型の定数 startLevel を宣言し、値として 1 を定めています。

これで、後から startLevel = 2; などと書いて値を変更しようとしても コンパイルする時に、コンパイラがエラーを出してくれます。

関数の引数をイミュータブルにしよう

扱う変数をちゃんとイミュータブルであると宣言するとどんなメリットがあるでしょうか。

関数へアドレス渡しをすると、 渡した先の関数の中で、渡すときにセットした引数のメモリアドレスが指す中身が変更されてしまうことがあります。

便利なのですが、ふとした時にバグとなる場合があります。

例:最初のレベル・体力とレベルアップのレベル・体力を表示する例

 
not_immutable.c
#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);
}

以下のような結果を期待します。

  • レベル 1 なら firstHitpoint の中身を表示するのでHPは 10
  • レベル 2 なら nextHitpoint の中身を表示するのでHPは 18

ところが、これは予期せず下のような結果になってしまいました。

実行結果
レベル 2, HP = 10
レベル 2, HP = 18

レベル 1 の時の結果が表示されておらず、いきなり 2 から始まっています。

これは、getNextLevelHitpoint 関数のなかで、渡された レベルを表す変数 level のアドレスの中身を直接書き換えてしまっているからです。

具体的には 4 行目で、次のレベルの計算をするときに アドレス渡しされた level の変数の格納されているメモリアドレスの中身を + 1 してしまっています。

渡されたメモリアドレスの中身はアドレスするだけとし、更新をしないようにコーディングすることとし、 引数の中身を間違って変更しないように型修飾子である const を付けましょう。

下は変更後のイメージです。

 
immutable.c
#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関数にポインタを渡す必要はありませんので、 アドレス渡しをせずに下の用に書いてあげるのがよいですね。

 
immutable2.c
#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)は純粋関数です!

 
not_pure.c
#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

ページの更新履歴

更新日 更新内容
更新なし
イチからゲーム作りで覚えるC言語
第2章40 swap関数を作ってポインタ変数の中身を交換する : PREV
NEXT : 第2章42 ポインタへのポインタ :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

第4章3 C言語コンソール上で迷路脱出プログラム・迷路の自動生成

#C11仕様#C言語#ゲームプログラミング✎ 2021-05-15
C言語のコンソールゲームで迷路脱出ゲームプログラムの作り方を確認します。迷路は自動生成されます
広告領域
追従 広告領域
目次
第2章41 副作用がある関数、純粋関数、イミュータブル
第2章41 副作用がある関数、純粋関数、イミュータブル
Summury
Summury
副作用のある関数とは
副作用のある関数とは
副作用がある関数への注意
副作用がある関数への注意
イミュータブルってなに?
イミュータブルってなに?
関数の引数をイミュータブルにしよう
関数の引数をイミュータブルにしよう
純粋関数
純粋関数
非常に参考になったサイトさまや、参考文献など
非常に参考になったサイトさまや、参考文献など
ページの更新履歴
ページの更新履歴
Nodachisoft © 2021