🙌

Functional Programming in C# まとめ 4章

2024/02/21に公開

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

3章のまとめは、こちら

4. Patterns in functional programming

この章では以下を扱う。

  • コア関数Map、Bind、Where、ForEach
  • functorとmonadの紹介
  • 様々なレベルの抽象化を行う

パターンは様々な問題を解決するために適用できるソリューションであり、この章で詳細するパターンは単純な関数である。ただし、関数型のプログラミングでは、コア関数とされ、随所で利用される関数である。

4.1. Applying a function to a structure's inner values

Map。IEnumerableでは、Selectと同じもの。IEnumerableでの関数シグネチャは以下

(IEnumerable<T>, (T -> R)) -> IEnumerable<R>

Optionでの関数シグネチャは以下

(Option<T>, (T -> R)) -> Option<R>

OptionのMapを使うと、値が存在しているかどうかをということを気にしなくてよくなる。というもの、Noneの場合には関数が適用されずにNoneが返り、値がある場合のみ、関数が適用され、その結果がSomeに入って返ってくるから。

4.2. Performing side effects with ForEach

ForEachは、基本的にMapと同じだが、戻り値を返さない関数Actionを適用したい場合に利用する。

つまり、副作用を持った関数を適用するということなので、関数型プログラミングとしては、何かの処理の途中でたくさん使うようなものではないと考えられる。おそらく、最初にMapを説明したので、その流れで2番目に紹介しているのでしょう。

なぜMppをオーバーロードせず、別メソッドを定義するのか?という説明あり。.NETの型推論の技術的な仕様として、型推論の際には、戻置り値の型は考慮されない。つまり、Func<T, R>Action<T>は区別されないそうです。このことから、オーバーロードにしてしまうと、自動で利用する関数が判別できず、クライアントコードでジェネリック引数を毎回指定する必要がでてしまう。それが煩雑なので、別名のメソッドを定義している、とのことです。

4.3. Chaining functions with Bind

Bindはとても重要な関数。

以下、単純な例

Option<double> Div(double a, double b) => b == 0 ? None : a / b;

var onece = Div(4, 2);
var twice =  onece.Map(d => Div(d, 2));
// => Option<Option<double>>型のSome(Some(1))

これを防ぐために、Bindを使う。Option型のBindのシグネチャは以下

(Option<T>, (T -> Option<R>)) -> Option<R>
var twice =  onece.Bind(d => Div(d, 2));
// => Option<bouble>型のSome(1)

IEnumerableの場合、Bindは、SelectManyと同じ。

4.3.4. The Return function

Return関数。通常の値をコンテナに詰めるだけの関数。Option型だとSome、IEnumerableでは色々な実装があるが、例えば、1という値をReturn関数に渡すと、[1](1要素のIEnumerableで渡した値が入ったもの)ができる。

FP初学者向けの書籍なのであえてだと思いますが、

  • Mapが理にかなった実装で定義できる型がfuntor
  • BindとReturnが定義できるの型がmonad

という定義になっています。functor則やmonad則は後で説明されるのだろうか。

4.4. Filtering values with Where

Option型に対してもWhereが定義できる。

これまで説明してきたcore関数とその名前の対応表

LaYumba.Functional(著者の自作ライブラリ) LINQ FPでの一般的なその他の呼び方
Map Select fMap,Project,Lift
Bind SelectMany FlatMap,Chain,Collect,Then
Where Where Filter
ForEach n/a Iter

4.5 Combining Option and IEnumerable with Bind

この節は、著者の考えとして、Option型をIEnumerableに変換できると便利な場合がある、という話題。

4.6. Coding at different levels of abstraction

値は、大まかに2つに分類できる

  • 通常の値
  • コンテナに入った値

コンテナは、IEnumerableやOptionなどのジェネリックな型。通常の値をコンテナに入れることで、"effect"を付与できる。"effect"が付与されることで、より抽象度の高い処理を書くことができるようになる。

"effect"は、「すごいHaskell 楽しく学ぼう!」の言葉を借りれば、「文脈」。

  • Optionは、値があるかもしれないし、ないかもしれないという文脈を持った値
  • IEnumerableは、同時に複数の値が存在しているという文脈を持った値
  • Funcは、実行すると、その時に値が得られる(遅延する)文脈を持った値
  • Taskは、非同期性という文脈を持った値

通常の値だけを使って実装を行うと効率が悪く、間違いを犯しがちである。こういった抽象化をうまく使うことで、効率的で堅牢な実装を行うことができる。ただし、行き過ぎた抽象化も危険である。例えばA<B<C<D<T>>>>のような型は、非常に扱いずらい。これについては、13章で取り扱う。

著者の主張が分かりにくいのですが、FPに限らず、例えばプリミティブ型だけを使って実装したとしたら、効率が悪いので、OOPならクラスを作って、実装します。そうではなくて、特定の目的のために定義されたクラスより汎用的な型でありながら、文脈を持った型(OptionやIEnumerableなど)を上手に活用することで実装の効率があがる、ということ(ここまではFPとは無関係)。さらに、こういった文脈を持った型には、functorやmonadといった共通の数学的特性がある場合がある。これらはMapやBind(これらは高階関数)といった共通の操作で透過的に扱うことができるので、FPのテクニックを使うことで、実装の効率を上げることができる、という主張かと思います。

Discussion