🦁

Functional Programming in C# まとめ 6章

2024/02/27に公開

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

5章のまとめは、こちら

6. Functional error handling

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

  • Eitherを使った大体結果の表現
  • 失敗する可能性のある操作の連鎖
  • ビジネスエラーと技術的エラーを区別する

エラーハンドリングはアプリケーションの重要な部分であり、関数型と命令型のプログラミングスタイルで全く異なる。

  • 命令型プログラミングはthrowtry/catchのような特別なステートメントを使い、副作用を生み出す
  • 関数型プログラミングは、副作用を最小化するため努力する。だから、例外を投げるのは一般に避けられる。代わりに、操作が、成功か失敗か値で返す。

著者の意見では、例外は、"GOTO"文よりもたちの悪いのである、とのことです。

6.1. A safer way to represent outcomes

処理が失敗する理由が複数ありうるような場合、Optionでは失敗した理由がわからないので、不十分である。この問題に対処するために、Either型を使う。

Either<L, R> = Left(L) | Right(R)
  • Right:成功パス
  • Left:失敗パス

とするのが、FPの慣習であるので注意。

Eitherに対しても、コア関数を定義することができる。しかし、Map、Bindでは、Right(成功パス)のみに関数が適用される点に注意する。つまり、Left(失敗パス)の型はワークフローを通じて固定になる。

※この実装はバイアス実装(Rightを成功パスであり、関数の適用対象であると決めつけている)である。バイアスのない実装も存在する(Leftにも同様に適用するための関数を取り、適用する)。しかし、バイアス実装の方が汎用性が遥かに高い。

6.2. Chaining operations that may fail

関数型のパイプラインにおけるエラーハンドリングは、2路線システムとみなすことができる。※基本的には、Optionでパイプラインを構築したときと考え方は何も変わりません。

Railway Oriented Programming

正直このトピックに関しては、上の参照URLを見ていただくか、この記事と同じ方が書いている「Domain modeling made functional」という書籍を読まれた方が、詳しいし、わかりやすいです(実装例がF#ですが)。

6.3. Validation: a perfect use case for Either

実際のアプリケーションで、Leftの型は何にすべきか?という議論。前述のように、ワークフローを通じて、Leftの型は固定されるので、汎用的な型を考える必要がある。

  • 大抵の場合、文字列だけで十分だが、補足情報を入れたい場合もあるかもしれない
  • .NETのException型を使う。Leftに入るのは、ビジネスルールとしてのエラーなので、意味的におかしい。

ということで、著者のライブラリでは、以下のようなクラスを作っている。

namespace LaYumba.Functional
{
    public class Error
    {
        public virtual string Meaage { get; }
    }
}

さらに、個別のエラーごとにこれを継承したクラスを作成するのがお勧め、とのこと。

上で挙げた「Domain modeling made functional」では、Leftの型として判別共用体(discriminated union)を利用していました。ですので、「エラーのあるケースだけ、特殊なデータを付けたい」となっても、判別共用体にケースを追加することで簡単に対応できます。後にでてくる、ワークフローの途中で、Leftの型を変えたくなったら、どうするか、という議論についても同様に同じ型の別ケースとして追加することで対処できます。

判別共用体、C#に追加してほしいです。

6.4. Representing outcomes to client applications

関数パイプラインを実装するのに、Map、Bind、Whereを主に利用していて、Matchは殆ど利用していない。Map、Bind、Whereは、抽象化(コンテナ)の内部で利用するものであり、Matchは抽象化から出るために利用するもの、という違いがある。

一般的なルールとして、一度抽象化を行ったら、可能な限りそれを使い続けるのがよい。しかし、DB等のアプリケーション外部の仕組みを利用する際には、抽象化から出る必要がある(外部システムのAPI等がコンテナに対応していないので)。

つまり、ビジネスロジックは抽象化を使って実装し、ビジネスロジックと外部との境界まで抽象化を維持する。外部と通信する際に抽象化から外に出る、ということ。

6.5. Variations on the Either theme

Eitherには、いくつかの議論になりうる点がある:

  • Leftは常に同じ型である。異なるLeft型のEitherを返す関数をどう合成すればよいか?
  • 常に2つのジェネリック引数を指定する必要があり、コードがかなり冗長になる
  • 名前:Either、Left、Rightはあまりにも曖昧だ。もっとユーザーフレンドリーなものにできないのか?

Leftの型が違う関数を合成したい場合、Rightと同様にLeftにも関数を適用するMapをオーバーロードで作ることで解決できる。

public static Either<LL, RR> Map<L, LL, RR>
    (this Either<L, R> either, Func<L, LL> left, Func<R, RR> right)
        => either.Match<Either<LL, RR>>(
            l => Left(left(l)),
            r => Right(right(r)),
        );

※上に書いた通り、F#等の判別共用体を持つ言語であると、特に問題にならないケースになります。

残り2つの議論について

著者としては、FPの分野で一番普及していること、(様々な言語、実装で)仕様が安定していることなどを考慮して、Either型を使って説明をおこなった。また、Either型を理解しておけば、他の型(Eitherの亜種)にであったとしても問題なく理解できるであろうとのこと。

著者の作成したライブラリには、ジェネリック引数を1つ(Leftに対応する型は、固定されており指定しない)にし、名前をわかりやすくしたものを用意している。※他のライブラリでも、似たような型が存在する。

6.5.4. Leaving exceptions behind?

例外を使うことは有害であると認識されてきているので、より新しいプログラミング言語(GoやElixirやElm)はエラーは単に値として扱うべきという考え方を持っている。そもそもGoとElmには例外がない。C#においても例外があるからと言って使う必要はない。

では完全に例外はなくてもよいのか?というと、著者としては以下2つの場合には例外を使ってよい、としています。

  • 開発者のエラー:空のリストから項目を削除しようとしている場合など。
  • 設定エラー

開発者のエラーの意味が分かりずらいですが、「呼び出し元やさらにその上の階層のコードがキャッチして、何か別の処理に流す」ということを全く想定していないような例外であれば発生させてもよい(文字通りの例外)。※発生した場合には、アプリケーションが落ちる。ということかと思います。ただ、この場合、どこか途中の処理で例外が握りつぶされてしまう可能性もあるので、例外は難しい、ということになるのだと思います。

Discussion