#95 もしC#プログラマーがMaybeモナドを実装したら

皆さんは、「モナド」という言葉をご存じでしょうか。

第3回 mapからモナドを理解する - 本物のプログラマはHaskellを使う
モナドとは何か - モナドのすべて

よく分かりませんねw

私もまだ分かりきっていないんですが、「どうなるか分からない処理を包んで安全に操作するためのもの」としてモナドを認識しています。

でも、何が分からないかも分かっていないのは不安です。
ならば、これはどうか。

モナドを自分の知っている言語で実装したら、理解の助けになるんじゃないだろうか?

と言うことで、モナドをC#で実装してみようというのが今回のトピックです。

Maybeモナド


今回実装してみようと思っているのが、Maybeモナドです。
Maybeモナドとは、失敗するかもしれない処理、特にnullに触ってしまいそうな処理を安全に取り扱うものです。

F#をご存じの方なら、option型を思い浮かべてみてください。
option型は「値がないかもしれない」事を表現するために使います。

例えば、処理がちゃんと上手くいったときは値をSomeに包んで返し、失敗したときはNoneを返すtryHoge関数を考えます。
関数内でnullを触らないのでNullReferenceExceptionが発生する心配がありません。

tryHogeのような作りの関数を、戻り値がSomeかNoneかだけ判定して連結していく事を考えます。
最後まで成功していたら値はSomeで包んで返され、途中のどこかで失敗していたらNoneが返ってきます。
このルールを扱いやすくしたものが、Maybeモナドです。

参考:F#プログラマのためのMaybeモナド入門 - みずぴー日記

モナドを作る


さて、どうやってMaybeモナド、というかそもそもモナドを実装すれば良いでしょうか。
関数型言語であれば、モナドを実現する仕組みが初めから提供されていたりもするんですが、今回はC#です。
手作りなので、最低限の骨組みだけで構成してみます。
using System;

namespace MonadTrial
{
    public abstract class Maybe<T>
    {
        internal T value;
        public abstract T Value { get; }

        public static Maybe<A> Return<A>(A a)
        {
            return new Just<A>(a);
        }

        public static Maybe<B> Bind<A, B>(Maybe<A> ma, Func<A, Maybe<B>> amb)
        {
            return ma.Bind(amb);
        }

        public static T FromJust(Maybe<T> ma)
        {
            return ma.Value;
        }

        public abstract Maybe<B> Bind<B>(Func<T, Maybe<B>> amb);
    }

    public class Just<T> : Maybe<T>
    {
        public override T Value
        {
            get
            {
                return this.value;
            }
        }

        public Just(T a)
        {
            this.value = a;
        }

        public override Maybe<B> Bind<B>(Func<T, Maybe<B>> amb)
        {
            return amb(this.value);
        }
    }

    public class Nothing<T> : Maybe<T>
    {
        public override T Value
        {
            get
            {
                throw new NullReferenceException("Nothing has no value.");
            }
        }

        public override Maybe<B> Bind<B>(Func<T, Maybe<B>> amb)
        {
            return new Nothing<B>();
        }
    }
}
いろいろアレですが、簡単に説明します。

最低限のモナドを作るには、BindとReturnの2つがあれば良いかなと思っています。
(モナドにも色々種類があるので、それを実現させる際に他にも必要なものがあったりもします)

BindとReturnを、敢えてHaskellの型シグネチャを使って書いてみます。
Bind :: (Monad m) => m a -> (a -> m b) -> m b
Return :: (Monad m) => a -> m a
Bindは、ある型に対する操作の結果を次の操作に繋げるためのものです。
Returnは、値をある型で包むためのものです。
この「ある型」というのが、型としてのモナドですね。

で、今回のC#のコードでは、BindとReturnはおおよそ次のようなシグネチャを持ちます。
Maybe<B> Bind<A, B>(Maybe<A> ma, Func<A, Maybe<B>> amb)
Maybe<A> Return<A>(A a)
まずはBindから見ていきましょう。
Maybe<A>クラスのmaは、Aという型を値に持つMaybe型の引数、Func<A, Maybe<B>>クラスのambは、A型の値をとってMaybe<B>型の値を返す関数であるFunc型の引数です。
そして、戻り値はMaybe<B>型です。
またReturnでも、A型の値をとって、Maybe<A>型の値を返しています。
それぞれ、上記Haskellの型シグネチャに対応するように定義しています。

(FromJustメソッドやValueプロパティは値を取り出すためにありますが、説明は割愛しますw)

Maybeモナドを使ってみよう


さて、この手作りMaybeモナドを使ってみましょう。
namespace MonadTrial
{
    public class MaybeIntSample
    {
        public static Maybe<int> tryParse(string a)
        {
            var number = 0;
            if (int.TryParse(a, out number))
                return new Just<int>(number);
            else
                return new Nothing<int>();
        }

        public static Maybe<int> doMaybe(string a, string b)
        {
            return Maybe<int>.Bind(tryParse(a), (x) =>
                    Maybe<int>.Bind(tryParse(b), (y) =>
                        Maybe<int>.Return(x + y)));
        }

        public static Maybe<int> doMaybeThis(string a, string b)
        {
            return tryParse(a).Bind((x) =>
                    tryParse(b).Bind((y) =>
                        Maybe<int>.Return(x + y)));
        }
    }
}
.NET Frameworkには標準でInt32.ParseとInt32.TryParseが提供されています。
Int32.Parseを使うのは簡単ですが、渡した引数が数値に変換できない場合、FormatExceptionが送出されてしまいます。
Int32.TryParseを使えば、そのような場面でも例外が送出されることはありません。
ただし、変換後の値を格納するための変数をあらかじめ初期化して渡す必要があります。

もっと簡単に、数値の文字列形式をint型に変換したいッ!
そこで、上のようなtryParseメソッドを定義してみました。

文字列がちゃんとint型に変換できたときは、その値をJust<int>型に格納して返します。
もし変換に失敗したときは、Nothing<int>型のインスタンスを返します。

これを使った計算が、doMaybeメソッドおよびdoMaybeThisメソッドです。
引数として渡されたstring型のaおよびbがいずれも数値として適切であれば、それらを数値として加算した値がJust<int>に包まれて返ります。
一方、引数のいずれかが数値として不適切であれば、Nothing<int>のインスタンスが返ります。

Maybeモナドに頼らないなら


もし、このtryParseがなかったときの計算はどうなるでしょうか。
public static int manufacture(string a, string b)
{
    var numA = 0;
    var numB = 0;
    if (int.TryParse(a, out numA) && int.TryParse(b, out numB))
        return numA + numB;
    else
        throw new HogeException("数字じゃねぇもの食わせんじゃねぇ!!");
}
変換に失敗したときに例外を送出するのが良いかどうか、異論があると思いますが、だいたいこうなるんじゃないでしょうか。
まだ与える文字列が2つなので、そう見難くはなっていませんね。

しかし、これが3つ、4つと増えていくとどうなるでしょうか。
public static int manufactureWith4Args(string a, string b, string c, string d)
{
    var numA = 0;
    var numB = 0;
    var numC = 0;
    var numD = 0;
    if (int.TryParse(a, out numA) &&
        int.TryParse(b, out numB) &&
        int.TryParse(c, out numC) &&
        int.TryParse(d, out numD))
        return numA + numB + numC + numD;
    else
        throw new HogeException("数字じゃねぇもの食わせんじゃねぇっつってんだろ!!");
}
どうでしょう。
好みもあるとは思いますが、私はあまり好きじゃありません。

これを、tryParseを使って書いてみます。
public static Maybe<int> doMaybeWith4Args(string a, string b, string c, string d)
{
    return Maybe<int>.Bind(tryParse(a), (x) =>
            Maybe<int>.Bind(tryParse(b), (y) =>
                Maybe<int>.Bind(tryParse(c), (z) =>
                    Maybe<int>.Bind(tryParse(d), (w) =>
                        Maybe<int>.Return(x + y + z + w)))));
}
または、
public static Maybe<int> doMaybeThisWith4Args(string a, string b, string c, string d)
{
    return tryParse(a).Bind((x) =>
            tryParse(b).Bind((y) =>
                tryParse(c).Bind((z) =>
                    tryParse(d).Bind((w) =>
                        Maybe<int>.Return(x + y + z + w)))));
}
と、こうなります。
んー、万人に自信を持って綺麗だと言えないかもw
(私は好きです、変態なので)

でもまぁ、コード量はだいぶ減ったんじゃないかなと思います。
さらに、コード中のどこにも「もし処理の途中で失敗したら」ということを意識していません。
そういう事は、すべてMaybe型が引き受けてくれているのです。

おまけ


参考リンクに上げさせていただいた、みずぴーさんのF#プログラマのためのMaybeモナド入門を、もう1つの使用例に挙げてみます。
using System.Collections.Generic;

namespace MonadTrial
{
    public class DictionarySample
    {
        public static Maybe<T> tryFind<S, T>(S a, Dictionary<S, T> dict)
        {
            T value = default(T);
            if (dict.TryGetValue(a, out value))
                return new Just<T>(value);
            else
                return new Nothing<T>();
        }
    }
}
tryFindは、F#のMap.tryFindを意識して書きました。

これを使ってみます。
db = new Dictionary<string, int>();
db.Add("x", 1);
db.Add("y", 2);
db.Add("z", 3);
var result = Maybe<int>.Bind(tryFind("x", db), (x) =>
                Maybe<int>.Bind(tryFind("y", db), (y) =>
                    Maybe<int>.Return(x + y)));
このresultを見てもらうと、Just<int>型でValueプロパティが3のインスタンスになっているはずです。

お気づきになった方もいらっしゃるかもしれませんが、みずぴーさんのエントリでコンピュテーション式を展開した式が記載されています。
これとほとんど同じ形になっているのが分かるのではないでしょうか。

まとめ


頑張ってC#でMaybeモナドを実装し、使ってみました。
それなりに動作するものは作れたかなと思っています。
ただ、関数型言語で提供されているような、記述レベルで簡単に書けるほどではないですね。

なお、今回は盛り込みませんでしたが、一応このMaybeモナドは所謂モナド則を満たしているようです。
それらの確認を含めた全てのソースコードは、以下のリンク先にあります。

Gab-km / MonadTrial-on-CSharp

バグやイケてないところがあったら、ぜひ修正指摘をあげてくださいww

とりあえず、C#のようなオブジェクト指向(型マルチパラダイム)言語でも、モナドを構成することは出来るのです。
モナドは関数型言語だけのものではなく、仕組みもしくは考え方に過ぎない、ということです。

結論


F#!F#!

トラックバックURL

#95 もしC#プログラマーがMaybeモナドを実装したら へのトラックバック

まだトラックバックはありません。

#95 もしC#プログラマーがMaybeモナドを実装したら へのコメント一覧

まだコメントはありません。

コメントする

#95 もしC#プログラマーがMaybeモナドを実装したら にコメントする
絵文字
プロフィール
あわせて読みたい
あわせて読みたい
記事検索
Project Euler
なかのひと
アクセス解析
Coderwall
  • ライブドアブログ

トップに戻る