C# 続・メソッドチェーンしたくて仕方なかった
はじめに
前回の続きです。
前回 C# メソッドチェーンしたくて仕方なかった - さんぽみち
これを拡張したり根本的に変えたりしたら面白いものができたので記事にしてみます。
前回のあらすじ
メソッドチェーン楽しい
↓
途切れさせるやつ大っ嫌い
↓
チェーンにデリゲート渡して向こう側で処理すればずっとチェーンしてられるんじゃね?
↓
できた!
今回の特徴
前回の状態では、まだ使い勝手の悪い?場所があります。
一つ目は、例外を扱うクラスを綺麗に扱うことが難しいです。
例外を吐いた時のために、チェーンの外側でトラップしてやらなければなりません。
この例外について、復旧したり握りつぶしたりするのもチェーンの中でやっちゃいます。
二つ目は、条件分岐を記述する場合、ラムダ式が泥臭くなることです。
メソッドチェーンで埋め込む式は、できれば1文、多くても2文くらいにしたいです。
中で分岐などをし始めると汚くなります。
これを満たそうとすると、2分岐を三項演算子で表現するのが精いっぱいです。
そこで、分岐に関する処理をもっと戦略的かつ視覚的にもわかりやすくしようと思います。
なお、このように書きましたがすべてをチェーンで書くということ自体が変な考えであり、
チェーンを一度止めて判断したり例外は外でわかりやすくトラップするのが美しいコードだとは認識しております。
ですのでこの方向性自体には突っ込まないでください。
実装
少し長いですが一気に貼っちゃいます。
using System; using System.Linq; using System.Collections.Generic; namespace MaybeChain { public delegate T ConstFunc<T>(); public delegate void VoidFunc(); ///<summary>Maybe型を扱いやすくする為のメソッド群</summary> public static class Maybe { ///<summary>チェーンを始める為にMaybeで包む</summary> ///<param name="a">チェーンの先頭となる値</param> ///<returns>Maybeで包まれた値</returns> ///<remark>Wrap :: T -> Maybe T</remark> public static Maybe<T> Wrap<T>(this T a){ return Maybe.Return(a); } ///<summary>値をMaybeの世界に入れる</summary> ///<param name="a">対象となる値</param> ///<returns>Maybeとして扱うことのできる値</returns> ///<remark>Return :: T -> Maybe T</remark> public static Maybe<T> Return<T>(T a){ return Maybe<T>.Return(a); } ///<summary>一時的な値をスコープ限定で使いまわす</summary> ///<param name="a">使いまわしたい値</param> ///<param name="f">値を使いまわす関数</param> ///<returns>関数の適用結果</returns> ///<remark> ///2回だけ同じ値を使いたい場合などに使います。 /// ex) /// var temp = Calc(a); /// var max = temp > thresholds ? temp : thresholds; /// ↓ /// var max = Calc(a).With(x => x > thresholds ? x : thresholds); ///また、括弧が長くなるときにも有用です。 ///今回は仕方ないのでここに定義しますが、意味としてはここへの定義は不適切です。 ///</remark> public static U With<T, U>(this T a, Func<T, U> f){ return f(a); } } ///<summary>処理が失敗した時にその後の処理へ失敗した情報を伝播するクラス</summary> public abstract class Maybe<T> { #region ********** Maybeに関する定義 ********** protected abstract T Value{ get; set; } public abstract bool IsNothing{ get; } ///<summary>値をMaybeの世界に入れる</summary> ///<param name="a">対象となる値</param> ///<returns>Maybeとして扱うことのできる値</returns> ///<remark>Return :: T -> Maybe T</remark> public static Maybe<T> Return(T a){ return new Just<T>(a); } ///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark>Bind :: Maybe T -> (T -> U) -> Maybe U</remark> public Maybe<U> FMap<U>(Func<T, U> f){ return this.Bind( a => new Just<U>(f(a)) ); } ///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark> Bind :: Maybe T -> (T -> Maybe U) -> Maybe U</remark> public Maybe<U> Bind<U>(Func<T, Maybe<U>> f) { if(this.IsNothing) { // すでに処理が失敗している場合、型だけ変換して返す return new Nothing<U>(((Nothing<T>)this).Exception); } else { try { // 成功したら値が返る return f(this.Value); } catch(Exception ex) { // 失敗したらNothingが返る return new Nothing<U>(ex); } } } #endregion #region ********** アクション実行の定義 ********* ///<summary>戻り値のない関数を実行させる</summary> ///<param name="f">させたい処理</param> public void Execute<U>(Func<T, U> f){ if(!this.IsNothing) { f(this.Value); } } ///<summary>戻り値のない関数を実行させる</summary> ///<param name="f">させたい処理</param> public void Execute(Action<T> f){ if(!this.IsNothing) { f(this.Value); } } ///<summary>戻り値及び引数のない関数を実行させる</summary> ///<param name="f">させたい処理</param> public void Execute(VoidFunc f){ f(); } #endregion #region ********** チェーンに関するメソッド群 ********* ///<summary>チェーンを終了してMaybeから値を取り出す</summary> ///<exception cref="System.Exception">チェーン中に発生した最初の例外</exception> ///<returns>Maybeから取り出した値</returns> ///<remark>Tear :: Maybe T -> T</remark> public T Tear(){ return this.Value; } ///<summary>チェーンを終了してMaybeから値を取り出し、無効であった場合はデフォルトの値を返す</summary> ///<param name="defaultValue">値が無効であった時に返す値</param> ///<returns>Maybeから取り出した値</returns> ///<remark>Tear :: Maybe T -> T -> T</remark> public T Tear(T defaultValue){ return this.IsNothing ? defaultValue : this.Value; } ///<summary>Just型とNothing型についてパターンマッチングを行って処理を分岐させる</summary> ///<param name="whenJustCallback">Justであった場合に呼び出すコールバック</param> ///<param name="whenNothingCallback">Nothingであった場合に呼び出すコールバック</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> public Maybe<T> Match(Action<T> whenJustCallback, Action<Exception> whenNothingCallback) { if(this.IsNothing){ whenNothingCallback(((Nothing<T>)this).Exception); } else { whenJustCallback(this.Value); } return this; } ///<summary>チェーン中に発生した例外を捕まえる</summary> ///<param name="whenExceptCallback">例外発生時に行う処理</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> public Maybe<T> Catch(Action<Exception> whenExceptCallback) { if(this.IsNothing){ whenExceptCallback(((Nothing<T>)this).Exception); } return this; } ///<summary>チェーン中に発生した例外を握りつぶす</summary> ///<param name="defaultValue">例外発生時に差し替える値</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> public Maybe<T> Rescue(T defaultValue) { if(this.IsNothing){ return Maybe.Return(defaultValue); } return this; } ///<summary>アンマネージドなインスタンスを扱った処理を行う</summary> ///<param name="disposable">IDisposableを実装したインスタンスを返す関数</param> ///<param name="f">適用したい関数</param> ///<typeparam name="D">アンマネージドクラス</typeparam> ///<returns>関数を適用した後の値</returns> ///<remark>Using :: T -> D -> (T -> D -> U) -> U</remark> public Maybe<U> Using<D, U>(ConstFunc<D> disposable, Func<T, D, U> f) where D : IDisposable { try { using(D d = disposable()) { return this.FMap( b => f(b, d) ); } } catch(Exception ex) { return new Nothing<U>(ex); } } ///<summary>チェーンで計算させる</summary> ///<param name="f">適用したい関数</param> ///<returns>計算後の値</returns> ///<remark>Calc :: Maybe T -> (T -> U) -> Maybe U</remark> // public Maybe<U> Calc<U>(Func<T, U> f){ return this.FMap(f); } // fmapそのものである為不要 ///<summary>チェーンを途切れさせずに副作用のみ持つ関数を実行する</summary> ///<param name="f">行いたい処理</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> ///<remark>Action :: T -> (T -> U) -> T</remakr> public Maybe<T> Action<U>(Func<T, U> f){ this.FMap(f); return this; } ///<summary>チェーンを途切れさせずに副作用のみ持つ関数を実行する</summary> ///<param name="f">行いたい処理</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> ///<remark>Action :: T -> (T -> Void) -> T</remakr> public Maybe<T> Action(Action<T> f){ this.Execute(f); return this; } ///<summary>チェーンを途切れさせずに副作用のみ持つ関数を実行する</summary> ///<param name="f">行いたい処理</param> ///<returns>チェーンを途切れさせない為の、自身への参照</returns> ///<remark>Action :: T -> (Void -> Void) -> T</remakr> public Maybe<T> Action(VoidFunc f){ f(); return this; } ///<summary>同じ計算を複数回行わせる</summary> ///<param name="n">繰り返す回数</param> ///<param name=f>適用したい関数</param> ///<returns>複数回適用後の値</returns> ///<remark>Repeat :: Maybe T -> (T -> T) -> Maybe T</remark> public Maybe<T> Repeat(int n, Func<T, T> f) { return n > 0 && !this.IsNothing ? this.FMap(f).Repeat(n - 1, f) : this; } ///<summary>計算に使った全ての情報を返す</summary> ///<param name="f">適用した関数</param> ///<returns>元の値、適用した関数、適用結果をまとめたタプル</returns> public Maybe<Tuple<Maybe<T>, Func<T, U>, Maybe<U>>> All<U>(Func<T, U> f) { return Maybe.Return(Tuple.Create(this, f, this.FMap(f))); } public Maybe<T> When(Func<T, bool> doExecCallback, Func<T, T> f) { return this.FMap(doExecCallback).With( a => !a.IsNothing && a.Value ) ? this.FMap(f) : this; } public Maybe<U> When<U>(BranchStrategy<T, U> strategy) { return this.FMap(strategy.Run); } #endregion } ///<summary>値が有効であることを示すクラス</summary> public class Just<T> : Maybe<T> { public Just(T a){ this.Value = a; } protected override T Value{ get; set; } public override bool IsNothing{ get{ return false; } } } ///<summary>値が無効であることを示すクラス</summary> public class Nothing<T> : Maybe<T> { public Nothing(Exception ex){ this.Exception = ex; } protected override T Value{ get{ throw this.Exception; } set{} } public override bool IsNothing{ get{ return true; } } public Exception Exception{ get; set; } } ///<summary>分岐戦略を扱うクラス</summary> public class BranchStrategy<T, U> { private List<Tuple<Func<T, bool>, Func<T, U>>> strategyMap; private Tuple<Func<T, bool>, Func<T, U>> otherwise; ///<summary>デフォルト処理と戦略表からクラスを初期化する</summary> ///<param name="otherwise">いずれの条件にも当てはまらなかった場合に実行される処理</param> ///<param name="strategyMap">条件とその場合の処理をレコードとしたリスト</param> ///<remark> ///このコンストラクタを使うと、ラムダ式と型推論の相性や可変引数とタプルの相性が悪い為、非常に冗長な書き方となります。 /// ex) /// private static BranchStrategy<int, string> pointStrategy = /// new BranchStrategy<int, string>(point => "red point" /// ,Tuple.Create<Func<int, bool>, Func<int, string>>(point => point == 100, point => "very good") /// ,Tuple.Create<Func<int, bool>, Func<int, string>>(point => point >= 80, point => "good") /// ,Tuple.Create<Func<int, bool>, Func<int, string>>(point => point >= 60, point => "normal") /// ,Tuple.Create<Func<int, bool>, Func<int, string>>(point => point >= 30, point => "bad") /// ); ///その為、もう一方のコンストラクタからインスタンス化し、そこからチェーンでAddメソッドを呼ぶほうが楽です。 /// ex) /// private static BranchStrategy<int, string> pointStrategy = /// new BranchStrategy<int, string>(point => "red point") /// .Add(point => point == 100, point => "very good") /// .Add(point => point >= 80, point => "good") /// .Add(point => point >= 60, point => "normal") /// .Add(point => point >= 30, point => "bad") /// ; ///</remark> public BranchStrategy(Func<T, U> otherwise, params Tuple<Func<T, bool>, Func<T, U>>[] strategyMap) { this.strategyMap = strategyMap.ToList(); this.otherwise = Tuple.Create((Func<T, bool>)( value => true ), otherwise); } ///<summary>デフォルト処理のみを指定してクラスを初期化する</summary> ///<param name="otherwise">いずれの条件にも当てはまらなかった場合に実行される処理</param> public BranchStrategy(Func<T, U> otherwise) { this.strategyMap = new List<Tuple<Func<T, bool>, Func<T, U>>>(); this.otherwise = Tuple.Create((Func<T, bool>)(value => true), otherwise); } ///<summary>条件と処理を追加する</summary> ///<param name="condition">値を入れると条件に合うかを判定する関数</param> ///<param name="calc">行う処理</param> ///<returns>チェーンでつなぐ為の自分への参照</returns> public BranchStrategy<T, U> Add(Func<T, bool> condition, Func<T, U> calc) { this.strategyMap.Add(Tuple.Create(condition, calc)); return this; } ///<summary>値を受け取り、これに応じた関数を適用して返すメソッド</summary> ///<param name="value">判断基準となる値</param> ///<returns>値に応じた関数を適用した値</returns> public U Run(T value) { return this.strategyMap.FirstOrDefault( strategy => strategy.Item1(value) ) .With( strategy => strategy ?? this.otherwise ) .Item2(value); } } }
Maybeモナドベースに実装しました。
例外を中でトラップするというので想像がついた方もいらっしゃるかもしれません。
軽くだけ使い方を紹介します。
using System; using MaybeChain; public static class MainApp { [STAThread] public static void Main() { var strategy = new BranchStrategy<int, string>(point => "red point") .Add(point => point == 100, point => "very good") .Add(point => point >= 80, point => "good") .Add(point => point >= 60, point => "normal") .Add(point => point >= 30, point => "bad") ; Console.WriteLine("点数を入力してください"); Console.ReadLine() .Wrap() .FMap( str => Convert.ToInt32(str) ) .When( strategy ) .Catch( ex => Console.WriteLine(ex.ToString()) ) .Rescue( "不正な値です" ) .Action( msg => Console.WriteLine(msg) ); } }
結果
1回目 点数を入力してください 30 bad 続行するには何かキーを押してください . . . 2回目 点数を入力してください 0 red point 続行するには何かキーを押してください . . . 3回目 点数を入力してください 91 good 続行するには何かキーを押してください . . . 4回目 点数を入力してください xxxx System.FormatException: 入力文字列の形式が正しくありません。 場所 System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) 場所 System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) 場所 System.Convert.ToInt32(String value) 場所 MainApp.<Main>b__b(String str) 場所 MaybeChain.Maybe`1.<>c__DisplayClass1`1.<FMap>b__0(T a) 場所 MaybeChain.Maybe`1.Bind[U](Func`2 f) 不正な値です 続行するには何かキーを押してください . . .
こんな感じの使用感です。
戦略的に分岐させ、例外が発生しても処理を続けられます。
それでは中身を少しずつのぞいてみます。
Maybeモナドの実装
一番のメインとなるMaybeモナドから。
class Maybe<T>
#region ********** Maybeに関する定義 ********** protected abstract T Value{ get; set; } public abstract bool IsNothing{ get; } ///<summary>値をMaybeの世界に入れる</summary> ///<param name="a">対象となる値</param> ///<returns>Maybeとして扱うことのできる値</returns> ///<remark>Return :: T -> Maybe T</remark> public static Maybe<T> Return(T a){ return new Just<T>(a); } ///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark>Bind :: Maybe T -> (T -> U) -> Maybe U</remark> public Maybe<U> FMap<U>(Func<T, U> f){ return this.Bind( a => new Just<U>(f(a)) ); } ///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark> ///関数の第一引数が自身であると考えて /// Bind :: Maybe T -> (T -> Maybe U) -> Maybe U ///</remark> public Maybe<U> Bind<U>(Func<T, Maybe<U>> f) { if(this.IsNothing) { // すでに処理が失敗している場合、型だけ変換して返す return new Nothing<U>(((Nothing<T>)this).Exception); } else { try { // 成功したら値が返る return f(this.Value); } catch(Exception ex) { // 失敗したらNothingが返る return new Nothing<U>(ex); } } } #endregion
まずはReturnメソッドから。
///<summary>値をMaybeの世界に入れる</summary> ///<param name="a">対象となる値</param> ///<returns>Maybeとして扱うことのできる値</returns> ///<remark>Return :: T -> Maybe T</remark> public static Maybe<T> Return(T a){ return new Just<T>(a); }
Returnメソッドでは、新しいJust型のインスタンスを返しています。
Justの具体的な実装はこうなっています。
///<summary>値が有効であることを示すクラス</summary> public class Just<T> : Maybe<T> { public Just(T a){ this.Value = a; } protected override T Value{ get; set; } public override bool IsNothing{ get{ return false; } } }
単純に、指定された型の値を格納するだけのコンテナです。
もう一つのMaybeを継承したクラスの実装も並べて書きます。
///<summary>値が無効であることを示すクラス</summary> public class Nothing<T> : Maybe<T> { public Nothing(Exception ex){ this.Exception = ex; } protected override T Value{ get{ throw this.Exception; } set{} } public override bool IsNothing{ get{ return true; } } public Exception Exception{ get; set; } }
こちらは値を持たず、アクセスしようとすると問答無用で例外を投げつけます。
ここからなんとなく、値が有効か無効かをIsNothingメソッドとか使いながら
判断するのかなーと想像してもらえると嬉しいです。
次はFMapメソッドを見てみます。
///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark>Bind :: Maybe T -> (T -> U) -> Maybe U</remark> public Maybe<U> FMap<U>(Func<T, U> f){ return this.Bind( a => new Just<U>(f(a)) ); }
読み解く前に軽く説明すると、このメソッドはLinqでいうSelectメソッドと同等のものです。
具体的には、コンテナから要素を取り出して、関数により変換後にまた同じコンテナへ格納し、
そのコンテナを返すという動きをします。
それでは実際に中身を読んでみます。
引数に、コンテナが格納する型から別の型(同じでも可)へ変換するデリゲートが渡されています。
それをFunc<T, U>のデリゲートからFunc<T, Maybe<U>>へ変換しています。
そしてその変換したデリゲートをBindメソッドへ渡しています。
次にBindメソッドの中身を見てみます。
///<summary>値が有効なときのみ関数へ通してその戻り値を得る</summary> ///<param name="f">値へ適用したい関数</param> ///<returns>値が有効であれば関数を適用したMaybeインスタンス、有効でなければNothingインスタンス</returns> ///<remark>Bind :: Maybe T -> (T -> Maybe U) -> Maybe U</remark> public Maybe<U> Bind<U>(Func<T, Maybe<U>> f) { if(this.IsNothing) { // すでに処理が失敗している場合、型だけ変換して返す return new Nothing<U>(((Nothing<T>)this).Exception); } else { try { // 成功したら値が返る return f(this.Value); } catch(Exception ex) { // 失敗したらNothingが返る return new Nothing<U>(ex); } } }
やってること自体はシンプルです。
もし、自身がNothing(無効な値を表す)なら例外内容はそのままにコンテナの型を変えます。
つまり、中の要素に触らず、渡された関数も使うことなく形だけ変えて処理が終わります。
次に、もし自身が有効であれば渡されたデリゲートをコンテナの中身へ適用して返そうとします。
渡されたデリゲートがMaybe型へ変換してくれるのでそのまま返せますね。
もし、このデリゲートを実行中に例外が発生した場合はその例外内容を覚えさせて
Nothingを返します。
こうすることで、例外が発生しても中断することなく、無効な値として扱うことができます。
仮にNothingに対してBindメソッドを呼び出されても、上のガードによって無効な値の中身に
触れられることはありません。そつなく無難にしのいでます。
また、中の値を得るためのValueプロパティはprotectedであるため、
外部から直接触れられることはありません。
中の値へ触れるには、このBindメソッドへ渡すデリゲートからのみとなります。
ちなみにこれらのメソッド名についてですが、一般的な名前を使っている為、
Returnが気持ち悪いとかは勘弁してください。
Wikipedia先生から引用させていただきます。
形式的には、モナドは型構築子 M とふたつの演算 bind と return から構成される(returnをunitと呼ぶこともある)。
return 演算は素な型の値を受け取って、構築子によりモナド的なコンテナに詰めて返す。これは モナド的な値を作成する。
bind 演算はモナド的な値と、素な型の値からモナド的な値への関数を受け取って、新しいモナド的な値を返す。
~ 出典:Wikipedia モナド (プログラミング) - Wikipedia ~
同じページにちょうどMaybeモナドについても記述されているので読んでみてください。
外界との橋渡し、WrapメソッドとTearメソッド
それでは次に、このMaybeの世界とC#の世界をつなぐ、WrapとTearメソッドについて見てみます。
まずはMaybeクラス(ジェネリックでない)のWrapメソッドとReturnから。
class Maybe
///<summary>チェーンを始める為にMaybeで包む</summary> ///<param name="a">チェーンの先頭となる値</param> ///<returns>Maybeで包まれた値</returns> ///<remark>Wrap :: T -> Maybe T</remark> public static Maybe<T> Wrap<T>(this T a){ return Maybe.Return(a); } ///<summary>値をMaybeの世界に入れる</summary> ///<param name="a">対象となる値</param> ///<returns>Maybeとして扱うことのできる値</returns> ///<remark>Return :: T -> Maybe T</remark> public static Maybe<T> Return<T>(T a){ return Maybe<T>.Return(a); }
Wrapメソッドは拡張メソッドとなっており、Maybe.Returnを呼び出しています。
この型引数はどんな型でもいいので、気軽にMaybeの世界へ入れるよう拡張メソッドとしています。
また、こちらでもわざわざReturnメソッドを実装しているのは型推論させるためです。
有名どころでは、Tuple.Createなどと同じ考え方です。
このメソッドがなければ、Maybe<T>.Returnを呼び出す際、型引数まで指定しなければ
Returnを呼び出せません。
例えばこんな感じ。
// 型推論をさせない場合 var maybe1 = Maybe<Dectionary<string, int>>.Return(new Dictionary<string, int>()); // 型推論させた場合 var maybe2 = Maybe.Return(new Dictionary<string, int>()); // おまけでvar型を使わなかった場合 Maybe<Dictionary<string, int>> maybe3 = Maybe<Dictionary<string, int>>.Return(new Dictionary<string, int>());
Dictionaryくらいならまだ楽な方ですが、これでもずいぶん手間です。
また、DictionaryのKeyがStringからintに変わったなどがあった場合、2箇所直さなければなりません。
varすら使わなかったらと思うと型推論を生かすって大切ですね。
次はTearメソッドを見てみます。
class Maybe<T>
///<summary>チェーンを終了してMaybeから値を取り出す</summary> ///<exception cref="System.Exception">チェーン中に発生した最初の例外</exception> ///<returns>Maybeから取り出した値</returns> ///<remark>Tear :: Maybe T -> T</remark> public T Tear(){ return this.Value; } ///<summary>チェーンを終了してMaybeから値を取り出し、無効であった場合はデフォルトの値を返す</summary> ///<param name="defaultValue">値が無効であった時に返す値</param> ///<returns>Maybeから取り出した値</returns> ///<remark>Tear :: Maybe T -> T -> T</remark> public T Tear(T defaultValue){ return this.IsNothing ? defaultValue : this.Value; }
こちらはMaybeのコンテナを破いて中身を取り出すメソッドで、オーバーロードが2つあります。
一つ目の引数無しのものは、中身が何であろうが無理やり取り出そうとします。
Maybeな世界の中では無効な値であろうが無難に流せていましたが、厳密なC#本来の
世界に帰ってきたときは無効なままではいられません。現実はつらいものです。
ですのでさっきのNothing<T>の実装を除いたときにわかるように、中身が無効な値で
あった場合は例外が投げつけられます。
これに対し、二つ目のオーバーロードではデフォルト値を引数として渡します。
これにより、中身が無効な値であった場合はデフォルト値を返して例外を握りつぶします。
例えば、コンフィグファイルの読み込みに失敗した場合にデフォルト値を返す、などの使い方では
こちらが使いやすいと思います。
長くなりそうなので今日はここまでで!
おわりに
この実装は、C#の中に新しい文法を作っている気分になれてなかなか楽しいです。
Maybeによるチェーンにとってメタ的な値(例えばRepeatメソッドの繰り返す回数など)にも、
Maybeの中身から 引っ張ってこれるような方法を考えたいと思います。
完成させていち早く快適なチェーンライフを送りたいです。
参考
yohshiy様(2013) 静的型付けでの型推論と動的型付けでの型チェック | プログラマーズ雑記帳
型推論に関する内容が書かれています。
C#のvar型はVBのVariant型やjavascriptのvar型とは意味が違います。
この記事では、それがどう違うのかという大切なところがわかりやすく説明されています。
yohshiy様(2014) C# やるなら LINQ を使おう | プログラマーズ雑記帳
同じくyohshiy様より。
自分的にはLinqと言えばここ!!ってほどLinqについて学ばせて頂きました。
Linqへの理解があるとこの記事がわかりやすくなる為、苦手な方にはこちらをおすすめします。
Gab_km様(2011) #95 もしC#プログラマーがMaybeモナドを実装したら « C# « a wandering wolf
初めてモナドというものを知った時に読ませていただきました。
自分の良く知っている言語で書かれていると安心感があります。