C# 6.0 で関数型プログラミングしてみる

昨今、一部(?)で行き過ぎた関数型プログラミング手法への批判みたいなものが繰り広げられているようです。この記事を見て「C#でもモナドモナド言うやつが増えてくるのか…」と思う人が居るかもしれません。気にしすぎかもしれません。個人的なスタンスとしては、役立つ機能なら原理原則にとらわれず使っていいと思っています。可読性とか保守性とかメンバスキルがどうとかは使う組織やグループで足並みをそろえればいいだけの話だと思います。関数型もオブジェクト指向もimmutableもmutableもみんなちがってみんないい、みたいに思ってます。ブログanoparaはダイバーシティを強力に推進します。みたいな。

本題です。

C# 6.0(.NET Framework 4.6以降?かな?)になっていろんな機能が追加されました。expression-bodied関数メンバ(関数を1行で書ける)や、
null条件演算子、String interporationなどなどかゆいところに手が届く機能が多数増えました。素直に歓迎したいと思います。

C#で関数型プログラミングをやるにあたってたぶん最も重要な変更は実はusing staticだと思います。これはインポートしたクラスのメンバ関数をダイレクトに書けるという機能なんですが、これ、C#には何故か無かったんですよね。恐らくは多用すると破滅的に解読が難しいコードが書けてしまうから、とかいう理由で避けていたのだと思いますが、今回導入に至ったのは多分関数型プログラミングを意識したものだったのではないでしょうか。たぶん…。

で、using staticが出来て何が嬉しいかというと、アドホック多相が書きやすいとか、複雑な働きをする関数をまるでnewなしのコンストラクタのように使えるなどということが出来るのが嬉しいと思います。そいで調べてみるとlanguage-extというライブラリがありました。

C# functional language extensions and 'Erlang like' concurrency system

らしいです。GitHubスター数はこの記事作成時点で760くらいですが、日本語圏のサイトでは触れているところは見つからないですね…。C#erは関数型に興味ないんでしょうか。

ともかく、これを利用するとimmutableなデータ構造や、Option, Either, Tryなどお馴染みのクラスが使えるみたいです。

個人的にC#でしんどいと感じていたのがnullの扱いで、Optionクラスだけでも使いたいなあと思ってやってみることにしました。以下、そこまで意味あるコードではないですがOptionを使ってみるサンプルです。

[TestFixture]
class SampleTest
{
    /// <summary>
    /// Immutable Map
    /// </summary>
    private Map<string, int> dict = 
        Map(Tuple("isoroku", 56),
            Tuple("kaneda", 17),
            Tuple("tetsuo", 22),
            Tuple("yamagata", 19),
            Tuple("decosuke", 32));
                   
    [Test]
    public void FunctionalCSTest()
    {
        var maybeIsorokuAge = dict.TryGetValue("isoroku").Map(a => a - 30);
        Assert.IsTrue(maybeIsorokuAge == Some(26)); // Option overrides == operator.

        var maybeTrue = maybeIsorokuAge.Match(
            Some: x => true,
            None: () => false);
        Assert.IsTrue(maybeTrue);

        var maybeNone = dict.TryGetValue("akira");
        Assert.IsTrue(maybeNone == None);

        var maybeZero = dict.TryGetValue("akira").Map(a => a + 10).IfNone(0);
        Assert.IsTrue(maybeZero == 0);


        var maybeSum = 
                from x in dict.TryGetValue("isoroku")
                from y in dict.TryGetValue("kaneda")
                from z in dict.TryGetValue("tetsuo")
                select x + y + z;
        Assert.IsTrue(Some(95) == maybeSum);

        maybeNone = 
                from x in dict.TryGetValue("isoroku")
                from y in dict.TryGetValue("akira")
                from z in dict.TryGetValue("testuo")
                select x + y + z;
        Assert.IsTrue(None == maybeNone);
    }

}

今詳しく順に説明していきます。まず、以下がimmutableなMapとタプルです。MapはDictionaryのようなキー→値の関連を表すデータ構造で、実装は(多分)ハッシュテーブルです。

private Map<string, int> dict = 
        Map(Tuple("isoroku", 56),
            Tuple("kaneda", 17),
            Tuple("tetsuo", 22),
            Tuple("yamagata", 19),
            Tuple("decosuke", 32));

immutableとは不変という意味です。immutableなMapに新たな要素を追加すると、新たな要素が追加された新しいMapが返却されます。値が変わらないことで何が嬉しいのかというと、副作用が無いのでデバッグや保守が楽になります。ただ、上手に使わないとメモリ食いになります。

以下は、Mapからキーを取り出した値から30を引くという処理です。

        var maybeIsorokuAge = dict.TryGetValue("isoroku").Map(a => a - 30);
        Assert.IsTrue(maybeIsorokuAge == Some(26)); // Option overrides == operator.

五十六さんが30歳若返って26歳になりました。ここで重要なことがいくつかあります。

まず、TryGetValueという関数(個人的にはもうちょっと短い名前にしてほしい)はOptionというラップされた型を返却します。Optionというのはnull値をうまく扱うために導入する単純なラッパークラスで、SomeとNoneという二種類のサブクラスがあります。値が無いことを表すのにnullを使うのはやめてNoneで表そう、値がある場合はSomeにしようという発想ですね。

なんで値が無いことを表すのにnullを使っちゃいけないのかというと、いわゆるぬるぽ(C#の場合はNullReferenceですか)の発生があったりとかそういう理由です。

「えっ、じゃあ空な値を返す場合は全部Optionとかいうラッパーで包むの?面倒だなあ」と思いませんか?思いましたね?それを解決するのがモナドという仕組みです。モナドというのは超テキトーな説明をすると「ラップしたクラスをラップしていないように扱うための仕組み」になります。

具体的には、ここではMap関数を使います。MapはLinqでいうSelectのようなものです。が、OptionのMap関数(拡張メソッド)は以下のように宣言されています。

public static Option<R> Map<T, R>(this Option<T> self, Func<T, R> mapper);

つまり、下記の文脈だと

        var maybeIsorokuAge = dict.TryGetValue("isoroku").Map(a => a - 30);

Mapに渡すのはFunc<int, R>型になります。Mapが返す値はOptionになります。お分かりいただけたでしょうか?Mapの中だけを見ればOptionというラッパクラスがあたかも無かったかのように扱われていますね。これが「ラップしたクラスをラップしていないように扱うための仕組み」です。戻り値がOptionなのでこの処理は続けて行う事ができます。

では、値が無い場合、つまりNoneの場合はどうなるのでしょうか?コードの順序が前後してしまいますが、以下になります。

        var maybeZero = dict.TryGetValue("akira").Map(a => a + 10).IfNone(0);
        Assert.IsTrue(maybeZero == 0);

dictには"akira"というキーが無いのでTryGetValueはNoneを返します。NoneをMapにして何か演算を加えても、答えは必ずNoneになります(そういう決まりになってます)。ということはつまり、Optionを使えば値が空であろうとなかろうと(nullであろうとなかろうと)気にせず処理を進められます。これが嬉しいところです。

では実際に計算処理が終わって値を取り出す段ではどのようにすればよいのでしょうか。最も簡単なのが代替値を与えてあげることです。それが、IfNoneです。

        var maybeZero = dict.TryGetValue("akira").Map(a => a + 10).IfNone(0);

上記では失敗した場合はゼロを返却することになります。あとは、下記のように値がある場合と無い場合とで処理を分けて実行させることもできます。

        var maybeTrue = maybeIsorokuAge.Match(
            Some: x => true,
            None: () => false);
        Assert.IsTrue(maybeTrue);

Scala等の経験がある人は、「これパターンマッチでは?」と一瞬思ったかもしれません。でも良く見てください。引数名を明示的に書いているだけです。残念でした。

今度は複数の値を扱ってみましょう。複数の値をmapで組み合わせるのは関数がどんどんネストしてしまって非常にしんどいことになります。なので、大体、シンタックスシュガーみたいな機能が用意されます。language-extの場合はLINQと組み合わせるみたいですね。

        var maybeSum = 
                from x in dict.TryGetValue("isoroku")
                from y in dict.TryGetValue("kaneda")
                from z in dict.TryGetValue("tetsuo")
                select x + y + z;
        Assert.IsTrue(Some(95) == maybeSum);

ここで、from - inで取ってくる値が一つでもNoneになれば答えもNoneになります。従来だったらこういう処理はif文で書かなければなりませんでしたが、こういう書き方をすると何がしたいのか非常にすっきりと表すことが出来ます。Scalaのfor式に比べたら冗長な気がしますが、現状のC#仕様の中では限りなく美しい記法になっていると思います。

まとめ

今回はC#の関数型ライブラリとしてlanguage-extを紹介しました。

感想ですが、発想としては面白いもののまだまだ荒削りで改善すべきところがたくさんあるように思います。特にC#の言語仕様的な縛りが大きいような。個人的にはOption関連だけでも積極的に使って行きたいのですが、仕事でプロダクションに使うのはまだまだ厳しいかなあ…という印象。

関数型を積極的に導入していくことの是非は冒頭で述べたようにそれぞれの組織やグループで考えるべきことだと思いますが、オブジェクト指向も関数型も得意とするところが違っていて、両方使えたらプログラムを綺麗に書ける場面がぐっと広がるなーと個人的には思っています。だから、C#にも関数型機能がもっと充実されたら嬉しいです。

C#だとラムダ式もずいぶん前から導入されてますし、メンバスキル的にも関数型はしっくりくるかもしれませんね。うちの会社はラムダ式って何?みたいな人が大勢な感じですけど。という自虐ネタで締めます。