Functional Programming in C# のメモ
本記事はFunctional Programming in C#を読んで私が重要と感じた部分を列挙したメモです。読みながらなるほどと思った点だけを列挙するため網羅性はありません。書籍の英語は平易でコードや解説も丁寧で読みやすいので興味ある方は是非書籍をお読みください。
Functional Programming in C#: How to write better C# code
- 作者: Enrico Buonanno
- 出版社/メーカー: Manning Publications
- 発売日: 2017/09/17
- メディア: ペーパーバック
- この商品を含むブログを見る
感想
関数型プログラミング言語ではなく、C#で関数型プログラミングをする方法が良くまとまっている。F#等の本だと文法の学習と関数型プログラミングの学習の両方が必要になり大変だが、よく知っているC#なので関数型プログラミングの学習だけに集中できた点が非常に良かった。
一方、関数型プログラミング言語に比べてC#では工夫を凝らす必要がある。型推論が不十分なので明示する必要があったりimplicit operatorの多用やクエリ式をモナド合成に使用するなどトリッキーに感じる部分もあり、C#アプリケーション全体を関数型プログラミングで記述するには障壁が大きい。部分部分ではビジネスシーンに活用できる知識もあるが、少なくとも各種モナドが公式で提供されてかつ関数型プログラミングのイディオムが言語仕様でサポートされるのを期待する。
たまたま流れてきたツイートに共感した。
https://twitter.com/skmruiz/status/1109452873170071552
PART 1 コアコンセプト
第1章 イントロ:関数型プログラミング
- C#ではメソッドを
Func
やAction
で取り扱えるので関数が第一級オブジェクトであり、高階関数を定義できる。 - 例えば
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ではなく有効な範囲しかとらないデータ型を受けるべき。- 戻り値の有無により
Func
とAction
を使い分けなければならないのはコードの重複を生むので、戻り値void
の代わりにUnit
を使うと便利。 Option
を返せば有効な値がないケースへの対処を強制できる。
第4章 関数型プログラミングのパターン
Map
はコンテナの中身の変形、Bind
はコンテナ同士の結合。コンテナがIEnumerable
ならMap
=Select
、Bind
=SelectMany
のこと。Option
を要素数が0~1のIEnumerable
と考えることで、Option
にもLINQと同じ関数を定義できる。- LINQと同じように、Optionでも変形やフィルタリングといった処理を通していき、最後はForEachやMatchなどで消費する。
- functor(関手)とはコンテナ型越しにその内部のオブジェクトを
Map
できるコンテナ型のこと。monad(モナド)とは同じようにBind
(とReturn
)できるコンテナ型のこと。Option
もIEnumerable
も関手でありモナドである。 - 関数はマッピングの抽象レベルによって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
よりも意味のあるエラーハンドリングが可能。Option
とEither
は非常に似ている。どちらも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)という。- 例えば
Option
のApply
は、T -> Rである関数をラップしたOption<T -> R>
に対してOption<T>
をApply
するとOption<R>
を得られるというもの。
- 例えば
Map
はApply
とReturn
で定義できるし、Apply
はBind
とReturn
で定義できるので、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章 遅延実行・継続処理・モナドの合成
第12章 ステートフルなプログラムと演算
- ステートフルなプログラムを書くには、関数が状態を受け取り新しい状態を返すようにして、呼び出し元の関数のローカル変数で状態を持つようにする。
第13章 非同期演算
Task<T>
は将来値が返るという効果を持ったコンテナ=モナドとして扱える。 ※大体Rxっぽくなる。
第14章 データストリームとReactive Extensions
(Rxの紹介なので流し読みしただけ)
第15章 メッセージパッシングの並列処理入門
(実用向けの内容のようなので未読)