Functional Programming in C# まとめ 2章

2024/02/15に公開

Functional Programming in C#の2章についてまとめています。

1章のまとめは、こちら

2. Why function purity matters

2章で扱う内容は以下

  • 関数が純粋であるか不純であるかは何で決まるか
  • 同時実行の際に、純粋さが問題になるのはなぜか
  • テスト可能性と純粋さはどう関係があるか
  • コードから不純さを取り除く

2.1. What is function purity

  • 純粋な関数:数学の関数のように、入力値が決まれば出力値が決まるもの
    • 出力値が入力値だけに依存する
    • 値を返す以外のことを何もしない
  • 不純な関数:副作用がある関数
    • 副作用の分類は以下
      • グローバル状態の変更:ここでのグローバルは、その関数のスコープの外を意味する
      • 入力引数の変更
      • 例外のスロー:契約である関数シグネチャで表現されないので
      • I/Oの実行:標準入出力、DB、ファイル、外部サービスなど
副作用の種類 対応方針
グローバル状態の変更 多くの場合避けることができる
入力引数の変更 関数型プログラミングに限らず、やってはダメ
例外のスロー 多くの場合避けることができる
I/Oの実行 避けることはできないが、I/Oを実行する処理を端へ追いやる

グローバル状態の変更、例外のスローについては、本書全体で詳しく説明しているそうです。

2.2. Purity and concurrency

本書は、FP導入の動機の大きな部分を並列実行においているので、並列実行時に純粋関数だどう役立つかを見ていく。

  • 純粋関数:何も手を加えなくても並列実行ができる
  • 不純関数:並列実行すると、結果がおかしくなったり、おかしくならなかったりする(どうなるかはわからない)

個人的には、処理全体をLINQでパイプラインにしておくと、並列化するときにもほとんど書き換えが必要ない、というところが.NET素敵だな、と感じた。

本書では、インスタンスメソッドをスタティックメソッドへ変更するリファクタリングが多数出てくるが、スタティックメソッドが問題になるケースは以下

  • ミュータブルなスタティックフィールドの役割を果たしている
    • これは、グローバル変数と変わらないので、スタティックメソッドの問題ではなく、やってはいけない
  • I/Oを実行する
    • テストができなくなってしまうので、避ける

関数が純粋である場合、関数をスタティックにすることに欠点は特にない。一般的なガイドラインは以下

  • 純粋な関数はスタティックにする
  • ミュータブルなスタティックフィールドは避ける
  • I/Oを実行するスタティックメソッドは直接呼び出さない

個人の経験としては、スタティックメソッドにしてしまうと、(オブジェクト指向における)インターフェースに入れられないので、モックすることができなくて困ったことがありました。が、FPの考え方では、モックオブジェクトは使わず、高階関数を使って依存性注入するのでスタティックメソッドにすることで問題が起こることがないのでしょう。

2.3. Purity and testability

関数が純粋かどうかとテストの関係を説明する。

  • 純粋関数:テストが簡単
  • 不純関数:テストが困難

不純関数は、入出力を以下のように考えることで、純粋関数とみなすことができる

入力は以下

  • 関数の引数
  • 参照しているプログラムの状態
  • 参照しているプログラムのスコープ外の状態

出力は以下

  • 関数の戻り値
  • 更新されたプログラムの状態
  • 更新されたプログラムのスコープ外の状態

入力を完全に再現し、すべての出力を検証すればテストすることができる。

が、これだと辛いので副作用を持つ部分は、

  • 実装方法を変えて、なくしてしまう
  • なくすことはできない場合は、分離して端においやってしまう

という方法で対処する。

分離する方法として、OOPではインターフェースを使って分離を行う。しかし、この方法だと大量のインターフェースと大量のボイラプレートコードを生み出してしまうので、著者はよい方法だとは考えていない。

ということで、インターフェースを利用する代わりに、関数を渡すことにする。これにより、大量のインターフェースを定義する必要がなく、ボイラプレートコードも削減しつつ、依存性注入を行うことができる。

別の話題として少し触れられていたものとして、パラメタライズドテストがある。パラメタライズドテストは、新しいテストケースが簡単に追加できるので便利である(テスト対象の使用が変わった時に追随しやすい)。だけでなく、パラメタライズドテスト自体が、関数になっている。

// Nunitを使ったパラメタライズドテストのコード例
[TestCase(1.80, 59, ExpectedResult = 18.21)]
[TestCase(1.80, 77, ExpectedResult = 23.77)]
[TestCase(1.60, 77, ExpectedResult = 30.08)]
public double TestCalcBmi(double hight, double weight)
    => Bmi.CalcBmi(hight, weight);

それぞれのケースでは、入力に対応する出力を定義していく。パラメタライズドテストは本質的には、テスト対象となる関数の単なるアダプタ。

ということで、それがプログラムとして良いことか悪いことかは別にしても

  • テストしづらい→関数的でないメソッドになっている

ということは言えるのかな、思いました。

Discussion