Functional Programming in C# のメモ

本記事はFunctional Programming in C#を読んで私が重要と感じた部分を列挙したメモです。読みながらなるほどと思った点だけを列挙するため網羅性はありません。書籍の英語は平易でコードや解説も丁寧で読みやすいので興味ある方は是非書籍をお読みください。

Functional Programming in C#: How to write better C# code

Functional Programming in C#: How to write better C# code

感想

関数型プログラミング言語ではなく、C#関数型プログラミングをする方法が良くまとまっている。F#等の本だと文法の学習と関数型プログラミングの学習の両方が必要になり大変だが、よく知っているC#なので関数型プログラミングの学習だけに集中できた点が非常に良かった。

一方、関数型プログラミング言語に比べてC#では工夫を凝らす必要がある。型推論が不十分なので明示する必要があったりimplicit operatorの多用やクエリ式をモナド合成に使用するなどトリッキーに感じる部分もあり、C#アプリケーション全体を関数型プログラミングで記述するには障壁が大きい。部分部分ではビジネスシーンに活用できる知識もあるが、少なくとも各種モナドが公式で提供されてかつ関数型プログラミングのイディオムが言語仕様でサポートされるのを期待する。

たまたま流れてきたツイートに共感した。

https://twitter.com/skmruiz/status/1109452873170071552

PART 1 コアコンセプト

第1章 イントロ:関数型プログラミング

  • C#ではメソッドをFuncActionで取り扱えるので関数が第一級オブジェクトであり、高階関数を定義できる。
  • 例えばusing句の代わりに下記の高階関数を定義すれば重複コードを減らせる。定型コードで挟むような場合一般に適用できるパターン。
public static R Using<TDisp, R>(TDisp disposable, Func<TDisp, R> func)
    where TDisp : IDisposable
{
    using (disposable)
        return func(disposable);
}
  • 関数とは入力から出力へのマッピングである。したがってDictionaryも関数として使える。

第2章 純粋関数が重要なワケ

  • 並列処理や自動テスト結果の一貫性のため、関数は純粋であることが望ましい。
  • I/O処理は外界に依存するため絶対に純粋関数にできないが、I/O処理を分離して注入することでそれ以外を純粋にできる。
internal static void Run(Func<string, double> readDouble, Action<string> write)
{
    var height = readDouble("enter height in metres.");
    var weight = readDouble("enter weight in kg.");
    // ...
    write(message);
}

第3章 関数のシグネチャと型の設計

  • シグネチャに期待される通りの挙動をするのが良い。
  • intを引数として受けておいて条件でArgumentExceptionを投げるくらいなら、intではなく有効な範囲しかとらないデータ型を受けるべき。
  • 戻り値の有無によりFuncActionを使い分けなければならないのはコードの重複を生むので、戻り値voidの代わりにUnitを使うと便利。
  • Optionを返せば有効な値がないケースへの対処を強制できる。

第4章 関数型プログラミングのパターン

  • Mapはコンテナの中身の変形、Bindはコンテナ同士の結合。コンテナがIEnumerableならMap = SelectBind = SelectManyのこと。
  • Optionを要素数が0~1のIEnumerableと考えることで、OptionにもLINQと同じ関数を定義できる。
  • LINQと同じように、Optionでも変形やフィルタリングといった処理を通していき、最後はForEachやMatchなどで消費する。
  • functor(関手)とはコンテナ型越しにその内部のオブジェクトをMapできるコンテナ型のこと。monadモナド)とは同じようにBind(とReturn)できるコンテナ型のこと。OptionIEnumerableも関手でありモナドである。
  • 関数はマッピングの抽象レベルによって4つに分類できる:
関数のシグネチャ マッピングの種類
T -> R 低レベル間でのマッピング 普通の関数
A<T> -> A<R> 高レベル間でのマッピング Map, IEnumerable.Select()
T -> A<R> 低レベルから高レベルへ引き上げるマッピング Return
A<T> -> R 高レベルから低レベルへ引き下げるマッピング Reduce, IEnumerable.Aggregate()
  • 異なる抽象レベルを混ぜないことが重要。例えば高レベルの話の中にforループやnullといった低レベルの操作が混ざると効率的でなく不具合の元である。

第5章 関数合成によるプログラム設計

  • LINQでif文を使わないのと同じく、Optionでもif文を使わずにメソッドチェーンで実装する。LINQではIEnumerableで連鎖させるのでシーケンスでなければ扱えないが、Optionを使えば単一の値・オブジェクトを扱える。
  • 命令型では文で実装するが、関数型では式で実装する。(そのために式をつなげる=関数合成のためにOptionといったインターフェースが必要)

PART 2 関数型らしく

第6章 関数型のエラーハンドリング

  • OptionがNone(無効)とSome(有効な値)の二分だったのに対し、EitherはLeft(エラーの値)とRight(正常な値)の二分であり、LeftはNoneとは異なり値を持つことができる。これによりOptionよりも意味のあるエラーハンドリングが可能。
  • OptionEitherは非常に似ている。どちらも2本の線路とみなすことができる。
  • Eitherは汎用的なので、エラーハンドリング用に特化した型引数を持つ型を作る/利用するのが良い。
  • アプリケーション外部との界面で抽象レベルと実体レベルとを相互変換する。アプリケーションのコアでは抽象レベルで扱う。

第7章 アプリケーションを関数で組み上げる

  • 関数へ引数を段階的に与えるには、与えたい引数をキャプチャさせたクロージャとして新たなFuncに包んでいけばよい。複数の引数を持つ汎用関数から段階的に引数を与えて専用関数を得るということ。
public static Func<T2, R> Apply<T1, T2, R>(this Func<T1, T2, R> f, T1 t1)
    => t2
    => f(t1, t2);
// ...
  • ただし、C#コンパイラの制約上メソッドに対して直接Applyできないので、メソッドとしてではなくFuncに包んで定義しておく必要があることに注意。
  • Curryでカリー化することで、複数の引数を取る関数を単一の引数を取る関数がネストされた状態に変換しても、段階的に引数を与えられるが、Applyの方が直感的である。
public static Func<T1, Func<T2, R>> Curry<T1, T2, R>(this Func<T1, T2, R> f)
    => t1
    => t2
    => f(t1, t2);
// ...
  • OOPでDIする際はオブジェクトを渡すが、FPでは関数を渡す。アプリケーション全体をFPで組み上げるには、全てのモジュール間の依存関係を関数の注入で解決するイメージ。

第8章 複数の引数を取る関数を効果的に使う

  • 入力も出力もコンテナ型のままApplyできるコンテナ型をapplicative (functor)という。
    • 例えばOptionApplyは、T -> Rである関数をラップしたOption<T -> R>に対してOption<T>ApplyするとOption<R>を得られるというもの。
  • MapApplyReturnで定義できるし、ApplyBindReturnで定義できるので、MonadならApplicativeでありFunctorである。
パターン 定義されている関数 関数のシグネチャ
Functor Map F<T> -> (T -> R) -> F<R>
Applicative Return T -> A<T>
Apply A<(T -> R)> -> A<T> -> A<R>
Monad Return T -> M<T>
Bind M<T> -> (T -> M<R>) -> M<R>
  • メソッドチェーンによるLINQでは複数のシーケンスを扱う場合にネストになってしまい可読性が落ちる場合に、クエリ式を使うと可読性を向上させられる。同様に、複数のモナドを扱う場合の可読性を確保するためにクエリ式を活用できる。
  • 特に複数のfrom句のあるクエリ式ではselect句はSelectManyすなわちBindになるため、Bind操作の可読性が向上する。

※感想:クエリ式でフラットに書けるのでモナドの合成もネストされず良い、と書かれているがトリッキーすぎて可読性があるとは思えない。

第9章 データを関数的に考える

  • データを不変にするためには3つの方法がある。
    • 単に変更しないようにする
    • 不変型として定義する
    • 型をF#で書いてしまう
  • データ単体だけではなくデータ構造も不変にする必要があり、そのためにはSystem.Collections.Immutableの型を使うのが良い。

第10章 イベントソーシング:関数型の永続化アプローチ

(イベントソーシングの紹介なので未読)

PART 3 高度なテクニック

第11章 遅延実行・継続処理・モナドの合成

  • 遅延実行の基本は、値を渡すのではなくFuncに包んで渡すこと。
  • Funcを受け取りその実行前後で何かするという関数をモナドに包み合成可能にできる。(TryモナドMiddlewareモナド

第12章 ステートフルなプログラムと演算

  • ステートフルなプログラムを書くには、関数が状態を受け取り新しい状態を返すようにして、呼び出し元の関数のローカル変数で状態を持つようにする。

第13章 非同期演算

  • Task<T>は将来値が返るという効果を持ったコンテナ=モナドとして扱える。 ※大体Rxっぽくなる。

第14章 データストリームとReactive Extensions

(Rxの紹介なので流し読みしただけ)

第15章 メッセージパッシングの並列処理入門

(実用向けの内容のようなので未読)