a wandering wolf

Does a wandering wolf dreams of a wondering, sometimes programming sheep?

このエントリーをはてなブックマークに追加

ゆとり世代のための C# プログラミング #yutori_history

はじめに

これは、 ゆとり Advent Calendar の10日目のエントリーです。

9日目は @y_uuk1 さんの ゆとりbotで学ぶ転職の極意 #yutori_history でした。

皆さんの記事を読んでいると、どうも技術系のエントリが少ないなぁと思いましたので、ここらで硬派な記事をゆとりさん(@megascus)に叩きつけたいと思います。

改めまして、ゆとりさん、転職おめでとうございます!ゆとりさんが次職でどのようなお仕事をされるのか期待が高まりますが、前職で一時期 C# を触られていたことを思い出します。

もしかしたら、また C# を触る可能性もあるので、「ゆとり世代のための C# プログラミング」というものをご提案しようと考えた次第です。

普通のプログラミング

さて、プログラミングでは常に正常系を書いてりゃいいというものではありません。だいたいが「上手くいかない場合」もセットで書く必要があるでしょう。

例えば、 Dictionary<string, int> 型のデータにアクセスし、計算結果を表示するような場合を考えてみます。

static void Main(string[] args)
{
    Dictionary<string, int> dict = new Dictionary<string, int>();
    dict.Add("hoge", 1);
    dict.Add("fuga", 2);

    int x, y, result = 0;
    var success = false;

    // dictから指定したキーに紐づく値を取得し、足し算します
    if (dict.TryGetValue("hoge", out x))
        if (dict.TryGetValue("fuga", out y))
        {
            success = true;
            result = x + y;
        }
        else
            success = false;
    else
        success = false;

    // 計算結果を表示します
    if (success)
        Console.WriteLine(result);
    else
        Console.WriteLine("failure");

    // 再びdictから値をとって、足し算します
    if (dict.TryGetValue("hoge", out x))
        if (dict.TryGetValue("bar", out y))
        {
            success = true;
            result = x + y;
        }
        else
            success = false;
    else
        success = false;

    // 足し算に失敗しているので'failure'を表示します
    if (success)
        Console.WriteLine(result);
    else
        Console.WriteLine("failure");
}

いかがですか?「これはひどい」と感じたはずです。ひどくなるように書いたからです(

それは冗談としても、野暮ったさは隠しきれませんね。書きたいことを書くために、いろんな「余分なもの」を書かないといけないので、何だか見通しが良くないコードになってしまいました(もちろんわざとです)。

ゆとり世代のためのプログラミング

そう、こんなまどろっこしいことは耐えられないのです。ゆとり世代のプログラマーには、もっと直感的なプログラミングでよろしくやってくれるやり方が必要とされています。

我々が求めていたもの、それはもうお気づきでしょう。

はい、 Option 型です。

public abstract class Option<T>
{
    /// <summary>
    /// <c>Some</c>型かどうかを取得します。
    /// </summary>
    public abstract bool IsSome { get; }

    /// <summary>
    /// <c>None</c>型かどうかを取得します。
    /// </summary>
    public bool IsNone
    {
        get { return !IsSome; }
    }

    /// <summary>
    /// <c>Some</c>型の場合、値を取得します。<c>None</c>型の場合、NullReferenceExceptionを送出します。
    /// </summary>
    public abstract T Value { get; protected set; }

    /// <summary>
    /// 渡された値を<c>Some</c>型で包んで返します。
    /// </summary>
    /// <param name="value"><c>Some</c>型に格納したい値</param>
    /// <returns><c>Some</c>型の値</returns>
    public static Option<T> Some(T value)
    {
        return value == null ? None<T>.Self : Some<T>.Create(value);
    }

    /// <summary>
    /// <c>None</c>型の値を返します。
    /// </summary>
    /// <returns><c>None</c>型の値</returns>
    public static Option<T> None()
    {
        return None<T>.Self;
    }

    /// <summary>
    /// 省略可能な値に対して関数を呼び出します。
    /// </summary>
    /// <typeparam name="U">出力型</typeparam>
    /// <param name="binder">オプションから型 T の値を受け取り、型 U の値を格納するオプションに変換する関数</param>
    /// <param name="option">入力オプション</param>
    /// <returns>バインダーの出力型のオプション</returns>
    public static Option<U> Bind<U>(Func<T, Option<U>> binder, Option<T> option)
    {
        return option.IsSome ? binder(option.Value) : Option<U>.None();
    }

    /// <summary>
    /// 現在のオブジェクトを表す文字列を返します。
    /// </summary>
    /// <returns>現在のオブジェクトを説明する文字列</returns>
    public abstract override string ToString();

    /// <summary>
    /// 擬似パターンマッチを提供します。
    /// </summary>
    /// <param name="someCase"><c>Some</c>型の値の場合に行う処理</param>
    /// <param name="noneCase"><c>None</c>型の値の場合に行う処理</param>
    public abstract void MatchWith(Action<T> someCase, Action noneCase);

    /// <summary>
    /// 擬似パターンマッチを提供します。
    /// </summary>
    /// <typeparam name="U">処理結果の型</typeparam>
    /// <param name="someCase"><c>Some</c>型の値の場合に行う処理</param>
    /// <param name="noneCase"><c>None</c>型の値の場合に行う処理</param>
    /// <returns>処理結果</returns>
    public abstract U MatchWith<U>(Func<T, U> someCase, Func<U> noneCase);
}

上記のようなクラスを定義しておきます。併せて、サブクラスである Some クラスと None クラスも定義します。

 public class Some<T> : Option<T>
 {
     public override bool IsSome
     {
         get
         {
             return true;
         }
     }

     private T value;
     public override T Value
     {
         get
         {
             return value;
         }
         protected set
         {
             this.value = value;
         }
     }

     private Some(T value)
     {
         this.value = value;
     }

     /// <summary>
     /// <c>Some</c>型の値を生成します。
     /// </summary>
     /// <param name="value"><c>Some</c>型に包みたい値</param>
     /// <returns>生成した<c>Some</c>型の値</returns>
     internal static Option<T> Create(T value)
     {
         return new Some<T>(value);
     }

     public override string ToString()
     {
         return string.Format("Some({0})", this.value);
     }

     public override void MatchWith(Action<T> someCase, Action noneCase)
     {
         someCase(value);
     }

     public override U MatchWith<U>(Func<T, U> someCase, Func<U> noneCase)
     {
         return someCase(value);
     }
 }

public class None<T> : Option<T>
{
    public override bool IsSome
    {
        get { return false; }
    }

    public override T Value
    {
        get
        {
            throw new NullReferenceException("値は null です。");
        }
        protected set
        {
            throw new InvalidOperationException("None<T> では操作出来ません。");
        }
    }

    /// <summary>
    /// <c>None</c>型の唯一のインスタンス
    /// </summary>
    static internal readonly Option<T> Self;

    static None() { Self = new None<T>(); }

    private None() { }

    public override string ToString()
    {
        return "None";
    }

    public override void MatchWith(Action<T> someCase, Action noneCase)
    {
        noneCase();
    }

    public override U MatchWith<U>(Func<T, U> someCase, Func<U> noneCase)
    {
        return noneCase();
    }
}

Option 型を導入することにより、先ほどのコードはどのように変わるでしょうか。

static void Main(string[] args)
{
    Dictionary<string, int> dict = new Dictionary<string, int>();
    dict.Add("hoge", 1);
    dict.Add("fuga", 2);

    // dictから指定したキーに紐づく値を取得し、足し算します
    var result1 = Option<int>.Bind(
        x => Option<int>.Bind(
            y => Option<int>.Some(x + y),
            TryGet(dict, "fuga")
        ), TryGet(dict, "hoge")
    );

    // 'Some(3)'を表示します
    Console.WriteLine(result1);

    // 再びdictから値をとって、足し算します
    var result2 = Option<int>.Bind(
        x => Option<int>.Bind(
            y => Option<int>.Some(x + y),
            TryGet(dict, "bar")
        ), TryGet(dict, "hoge")
    );

    // 足し算に失敗しているので'None'を表示します
    Console.WriteLine(result2);

    // おまけ。パターンマッチめいたもの
    MatchWith(result1);
    MatchWith(result2);
}

static Option<int> TryGet(Dictionary<string, int> dict, string key)
{
    return dict.ContainsKey(key) ? Option<int>.Some(dict[key]) : Option<int>.None();
}

static void MatchWith(Option<int> option)
{
    option.MatchWith(
        x => Console.WriteLine("We've got a value {0}!", x),
        () => Console.WriteLine("We haven't any values...")
    );
}

どうです、洗練されたコードになったんじゃないですか?理想を言えば、 Option<int>.Bind がネストしたところがフラットになれば言うことないんですが、このくらいなら及第点でしょう。

もしフラットに書きたいというのであれば、 C# で書かれたオススメのライブラリ があるので、使ってみてください。

まとめ

書きたいことだけをシンプルに書くというのは、無駄なことをしたくないゆとり世代のプログラマーにピッタリです。いろんなやり方があると思いますが、こうやって Option パターン(または Maybe パターンとも)を利用することで、場面に囚われず思い通りのプログラミングができます。

なお、「こんな Option クラスとかを毎回書くのは面倒臭いぜ!」という真にゆとった方は、 こちらのライブラリ をお使いになることをおすすめします。

ゆとりさん、次職でも素晴らしい C# プログラミングをお楽しみください。

11日目は @mike_neck さんです。