🗂

Functional Programming in C# まとめ 7章

2024/02/29に公開

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

6章のまとめは、こちら

7. Structuring an application with functions

この章では以下の内容を説明する

  • 部分適用とカリー化
  • メソッドの型推論の制限を回避する
  • 関数ごとの依存関係を考える
  • アプリケーションをモジュール化し合成する
  • リストを単一の値へ削減する

7.1. Partial application: supplysing arguments piecemael

部分適用は、複数引数の関数に引数を与えて、元の関数よりも引数の少ない関数を得ること(与えた引数の値は固定される)。

一般的なApply関数(部分適用を行う関数)の実装:

// 2引数関数の場合
public static Func<T2, R> Apply<T1, T2, R>
    (this Func<T1, T2, R> f, T1 t1)
    => t2 => f(t1, t2);

// 3引数関数の場合
public static Func<T2, T3, R> Apply<T1, T2, T3, R>
    (this Func<T1, T2, T3, R> f, T1 t1)
    => (t2, t3) => f(t1, t2, t3);

先頭にある引数から固定されていくので、汎用的なものから順番に並べていくのがよい。※例えば、割り算をする関数であれば、分母、分子の順に並べておけば、Apply関数で「与えられた数字を5で割る」関数などを簡単に作ることができる。

7.2. Orvercoming the quirks of method resolution

部分適用を利用する上でのC#の言語仕様的な問題点とそれをどう回避するか、という議論。

  • メソッドは、デリゲートやラムダと比べて型推論の性能が劣る(複数の引数を取る場合のみ)
  • メソッドでなく、フィールド、プロパティ、又は、関数を返すメソッドとして定義することで欠点を回避できる

7.3. Curried functions: optimized for partial application

カリー化は、以下のようなこと:

(T1, T2,..., Tn) -> R

カリー化した場合、以下のシグネチャを持つ

T1 -> T2 -> ... -> Tn -> R

実装したコードを理解するのがわかりやすいと思います:

// 2引数関数の場合
public static Func<T1, Func<T2, R>> Curry<T1, T2, R>
    (this Func<T1, T2, R> func)
    => t1 => t2 => func(t1, t2);

// 3引数関数の場合
public static Func<T1, Func<T2, Func<T3, R>>> Curry<T1, T2, T3, R>
    (this Func<T1, T2, T3, R> func)
    => t1 => t2 => => t3 => func(t1, t2, t3);

カリー化した後に、一度に引数を与えて、元の関数のように実行したい場合以下のようになります:

Func<int, int, int> f = (int a, int b) => a + b;

var curried = f.Curry()

curried(1)(2)   // => 3

多くの関数型プログラミング系の言語では、既定で関数がカリー化されているので、関数シグネチャの表現が以下のようになっている。

T1 -> T2 -> ... -> Tn -> R

様々な方法で部分適用することができる:

  • カリー化された形で関数を書く(都度、手書き)
  • Curryを使って関数をカリー化し、引数使ってカリー化された関数を実行する
  • Applyを使って1つずつ引数を与える

どのテクニックを使うかは好みの問題だ。しかし、著者の個人的な見解ではApplyを使うのが、最も直感的だと思っている。

7.4. Creating a partial-application-friendly API

サードパーティ製のライブラリを部分適用を使って使いやすくする実装例。ここでは、DapperのAPIインターフェースを改善している。

7.5. Modularizing and composing an application

アプリが大きくなるにつれ、それらをモジュール化し、コンポーネントに分解する必要がある。OOPとFPでモジュール化の方法がどう違うのかを見ていく。

7.5.1. Modularity in OOP

OOPでは、インターフェースを使って、依存性逆転のパターンを利用する。この方法で得られる利点は2つ:

  • 分離:インターフェースの実装を交換してもクライアントに影響はない
  • テスト可能性:インターフェースの実装を都合の良いもの似書き換えて簡単にテストできるようにする

(OOPの)依存性逆転には、かなり高いコストがかかる:

  • インターフェースの数が爆増し、ボイラプレートが追加され、コードを追うのが難しくなる
  • アプリケーションのブートストラップがかなり面倒になる
  • テスト可能性のための偽実装を作るのが複雑になる可能性がある

これらの複雑さを解消するために、IoCコンテナやモックフレームワークを導入することが多い。

7.5.2. Modularity in FP

FPでは、インターフェースは使用せず、関数(デリゲート)を使う。なぜなら、関数シグネチャがインターフェースの役割を果たすから。

7.5.3. Compareing the two approaches

関数型の方法では、依存性逆転のメリットは損なわれていない。また、OOPバージョンの課題をやわらげている。

  • インタフェースを定義する必要がない
  • テストにモックが必要ない

また、OOPのインターフェース分離の原則を厳密に適用していこうとすれば、結果として、1つだけのメソッドを持つインターフェースを大量に定義する必要がでてくる。シングルメソッドインターフェースはデリゲートと大差ないので、関数を注入する方が遥かに簡単ということになる。

著者は何も触れてはいませんんが、シングルメソッドインターフェースであっても、メンバ変数を持つことができるので、メンバ変数を変更するような副作用を持った具象クラスを定義することで、デリゲートだと実現しづらい処理を実装することは可能なのかな、と思いました。本書は関数型の本であり、状態変更≒悪という立場なので、その観点からは、デリゲートとできることに違いはない、となるのだと思います。

7.6. Reducing a list to a single value

値のリストを1つの値へ縮小することは一般的な操作である。FPの用語では、foldやreduceと呼ばれる。LINQではAggregateと呼んでいる。

※reduceには、「(整理して)変える」という意味もあるそうです。

この節の内容は、LINQのAggregateが理解できていれば問題ないので、割愛。

Discussion