SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

ScalaのOption[T]をC#で真似する - クエリ式で使う

前回、この記事を書きまして。

hiroronn.hatenablog.jp

とりあえず、ScalaのOptionを実装できました。ただ、Scalaにはforという便利な構文がありまして。

val someOpt: Option[String] = Some("aaa")
val noneOpt: Option[String] = None
val concat: Option[String] = for {
  val1 <- someOpt // ここは通る
  val2 <- noneOpt // Noneなので、展開できない
} yield { 
  //ここは、val1とval2両方が展開できないと実行されない
  val result = val1 + val2
  println(result)
  result
}

println(concat) // None

これをC#で真似できないと不便です。

これについて、C#で真似できないかなぁとTwitterに呟いたらコメントいただきました。

なんだと…?

C#LINQを使う場合、以前から拡張メソッド方式に慣れるようにしていたせいで、クエリ式の存在を完全に忘れていました。

これなら、LINQクエリ式が要求するシグネチャのメソッドで、Optionの関数を呼び出してやれば、C#でもOptionっぽいの作れるやん!と思ったので、試してみましょう。

なお、実装しながら記事を書くので、半端なところで終わったらごめんなさい。

前提知識

記事はおそらく長くなるので、前提知識を説明せずに丸投げします。

C#LINQで使われるクエリ演算子(クエリ式)

ufcpp.net

ScalaのOption

www.ne.jp

実装していく

名前空間の考え方

上で触れたとおり、クエリ式で書けるようにするためには、LINQのメソッド名に合わせたメソッド名や型定義での実装を提供する必要があります。

一方、関数型のような名前、MapやFlatMap*1というメソッドも用意したいです。そこで、Functionalという名前空間にMapなどのメソッドを用意し、Linqという名前空間LINQ仕様のメソッドを用意します。

名前空間 役割
FunFun.Data Optionの型定義と生成処理
FunFun.Data.Functional 関数型ライクな命名を行ったメソッド。実装はこちらに書く
FunFun.Data.Linq LINQに合わせた実装を提供する。できる限りFunFun.Data.Functionalのメソッドを呼ぶようにする

ScalaOptionで定義されている関数は、拡張メソッドとしてFunFun.Data.Functionalで提供されます。

Option型定義と生成処理

先にこれを用意します。最初に書いた私の記事通り、様々な事情によりOptionはIOptionというinterfaceとなっています。

using System;
using FunFun.Data.Internal;

namespace FunFun.Data
{
    /// <summary>
    /// Represents optional values.
    /// </summary>
    /// <typeparam name="A">Inclusion type in <see cref="Option"/></typeparam>
    public interface IOption<out A>
    {
        /// <summary>
        /// If this instance is None, return true, false otherwise
        /// </summary>
        bool IsEmpty { get; }
        /// <summary>
        /// return Option's data. If this instance is None, throw ArgumentNullException.
        /// </summary>
        /// <returns></returns>
        A Get { get; }
    }

    /// <summary>
    /// Represents IOption factory methods.
    /// </summary>
    public static class Option
    {
        /// <summary>
        /// create None object.
        /// </summary>
        /// <typeparam name="A"></typeparam>
        /// <returns></returns>
        public static IOption<A> None<A>()
        {
            return new None<A>();
        }

        /// <summary>
        /// create Some object.
        /// </summary>
        /// <typeparam name="A"></typeparam>
        /// <param name="value">Inclusion instance in <see cref="Option"/></param>
        /// <returns></returns>
        public static IOption<A> Some<A>(A value)
        {
            return value == null ? throw new ArgumentNullException("") : new Some<A>(value);
        }

        /// <summary>
        /// if <paramref name="value"/> is null, create None, create Some otherwise.
        /// </summary>
        /// <typeparam name="A"></typeparam>
        /// <param name="value">Inclusion instance in <see cref="Option"/></param>
        /// <returns></returns>
        public static IOption<A> Create<A>(A value)
        {
            return value == null ? None<A>() : new Some<A>(value);
        }
    }
}

SomeNoneの実装は、公開しても扱いづらいだけなので隠蔽しています。

using System;

namespace FunFun.Data.Internal
{
    /// <summary>
    /// Class `Some[A]` represents existing values of type A
    /// </summary>
    /// <typeparam name="A">Inclusion type in <see cref="Option"/></typeparam>
    internal sealed class Some<A> : IOption<A>
    {
        private readonly A _v;
        public Some(A value)
        {
            if (value == null)
            {
                throw new ArgumentNullException();
            }
            _v = value;
        }
        public bool IsEmpty => false;

        public A Get => _v;
    }

    /// <summary>
    /// Class `None[A]` represents non-existent values.
    /// </summary>
    /// <typeparam name="A">Inclusion type in <see cref="Option"/></typeparam>
    internal sealed class None<A> : IOption<A>
    {
        public bool IsEmpty => true;
        public A Get {
            get {
                throw new NullReferenceException("object is not exist.");
            }
        }
    }
}

単純なselect

さて、まずは、

IOption<string> strOpt = Option.Some("   hello   ");
var trimSpaceOpt =
    from str in strOpt
    select str.Trim();
Assert.AreEqual("hello", trimSpaceOpt.Get);

のように書けるところを目指します。

ちゃちゃっとテストを準備します。すると、

f:id:hiroronn:20190720153608p:plain

となり、クエリ式(LINQ構文)がないよ、と言われます。書いてある通りSelectメソッドを作ります。

Mapの用意

先にこちらを作成します。

using System;

namespace FunFun.Data.Functional
{
    /// <summary>
    /// Declare extension methods of <see cref="Data.IOption{A}"/>.
    /// </summary>
    public static class OptionExtensions
    {
        /// <summary>
        /// Returns a Some containing the result of applying <paramref name="f"/> to this IOption's value if this IOpton is nonempty. Otherwise return None.
        /// </summary>
        /// <typeparam name="A">original type</typeparam>
        /// <typeparam name="B">type of after applying f</typeparam>
        /// <param name="opt"></param>
        /// <param name="f"></param>
        /// <returns></returns>
        public static IOption<B> Map<A, B>(this IOption<A> opt, Func<A, B> f)
        {
            return opt.IsEmpty ? Option.None<B>() : Option.Some(f(opt.Get));
        }
    }
}

Select実装

今回用意するのは、LINQのSelectの中でもっとも単純なタイプです。

参考: Microsoft Docs

必要な定義は以下の通りです。

public static IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);

これの、IEnumerableIOptionに置き換えたものが必要となります。

とはいえ、やっていることはMapと全く一緒で、内部の型に変換関数を適用するだけです。よって、単にMapを呼び出します。

using System;
using FunFun.Data.Functional;

namespace FunFun.Data.Linq
{
    public static class OptionExtensions
    {
        public static IOption<B> Select<A, B>(this IOption<A> opt, Func<A, B> f)
        {
            return opt.Map(f);
        }
    }
}

追加後、テストコードにusing FunFun.Data.Linqを追加すると、

f:id:hiroronn:20190720155839p:plain

コンパイルが通り、テストに成功するようになります。素晴らしい。

Optionを連鎖させる

次は、二つのOptionの両方に値があったら、それを結合した値を取得する状態を目指します。イメージは以下の通りです。

[TestMethod]
public void TestDoubleSomeOption()
{
    var opt1 = Option.Some("first");
    var opt2 = Option.Some("second");
    var resultOpt =
        from str1 in opt1
        from str2 in opt2
        select str1 + str2;
    Assert.AreEqual("firstsecond", resultOpt.Get);
}

[TestMethod]
public void TestOneSomeOneNoneOption()
{
    var opt1 = Option.Some("first");
    var opt2 = Option.None<string>();
    var resultOpt =
        from str1 in opt1
        from str2 in opt2
        select str1 + str2;
    Assert.IsTrue(resultOpt.IsEmpty);
}

今は通りません。

f:id:hiroronn:20190720162039p:plain

今度はSelectManyが無いと怒られるので、作りましょう。

FlatMapの用意

FlatMapは一言で説明しがたいのですが…Option.mapは戻り値がBの関数を渡すのに対してOption.flatMapは戻り値がOption[B]となる関数を渡します。

…要するに、FlatMapの戻り値に対してMapやFlatMapをかけられるようにするためのもので、連鎖を前提に作られています。FlatMapが返したIOptionを、連鎖先のメソッドで使う形となります。

実装そのものは難しくないです。

using System;

namespace FunFun.Data.Functional
{
    /// <summary>
    /// Declare extension methods of <see cref="Data.IOption{A}"/>.
    /// </summary>
    public static class OptionExtensions
    {
        // ...
        // 省略
        // ...
 
        /// <summary>
        /// Returns the result of applying <paramref name="f"/> to this value if this is nonempty.
        /// Returns None if this is empty.
        /// Slightly different from <see cref="Map{A, B}(IOption{A}, Func{A, B})"/> in that <paramref name="f"/> is expected to return an IOption(which could be None).
        /// </summary>
        /// <typeparam name="A"></typeparam>
        /// <typeparam name="B"></typeparam>
        /// <param name="opt"></param>
        /// <param name="f"></param>
        /// <returns></returns>
        public static IOption<B> FlatMap<A, B>(this IOption<A> opt, Func<A, IOption<B>> f)
        {
            return opt.IsEmpty ? Option.None<B>() : f(opt.Get);
        }
    }
}

SelectMany実装

作ります。今回も最も単純なパターンをまず用意します。

参考:Microsoft Docs

必要な定義は以下の通りです。

SelectMany<TSource,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TResult>>)

これの、IEnumerableIOptionに置き換えたものが必要となります。FlatMapと一緒なのでそのまま呼び出します。

using System;
using FunFun.Data.Functional;

namespace FunFun.Data.Linq
{
    public static class OptionExtensions
    {
        public static IOption<B> Select<A, B>(this IOption<A> opt, Func<A, B> f)
        {
            return opt.Map(f);
        }

        public static IOption<B> SelectMany<A, B>(this IOption<A> opt, Func<A, IOption<B>> f)
        {
            return opt.FlatMap(f);
        }
    }
}

しかし、テストが通りません。ビルドエラーになります。

f:id:hiroronn:20190720170320p:plain

実装すべきオーバーロードが異なるようです。なので、別のを実装します。

SelectMany実装 その2

必要なのはこちらです。

SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TCollection>>, Func<TSource,TCollection,TResult>)

こんな感じで実装されています。

github.com

Optionで言うなら、中の値を二回変換する感じです。よって、実装はこれでいけます。

public static IOption<B> SelectMany<A, T, B>(this IOption<A> opt, Func<A, IOption<T>> selector, Func<A, T, B> resultSelector )
{
    return opt.FlatMap(o => selector(o).Map(t => resultSelector(o, t)));
}

もうちょっと綺麗に書ける気もしますが…

この実装を追加すると、テストが通るようになります。

力尽きた

一応、Whereを追加してみようと思いましたが、記事を書きながらやるのに限界を感じたので、いったん止めます。

実装はGitHubに上げたので、追加していきます。

github.com

まとめ

C#視点からだと、

  • C#のクエリ式は、いろんなリソースを同じように扱いたいという思想だが、これが意外と便利
  • 名前が合っていればよい、というDuck Typing的な考え方だったことを初めて知った
  • 自分でクエリ式に対応する場合、対応するLINQメソッドのシグニチャに合わせていけばいい(どれに合わせるか、資料がどこかにあるはず)
  • ScalaのOptionぐらいなら、意外と実装できるのではないか。使い勝手自体はScalaのforみたいな感じになる
  • これを実装したらLINQで呼べるよ、というようなinterfaceを用意しなかったのは偉い。必要な分だけ実装すればよいため、LINQ準拠のライブラリ提供が楽になっている(今回だと、Optionはシーケンスではないため、シーケンス関連のメソッドは実装を提供する必要が無い)

Scala視点からだと、 * forを標準で用意したのはすごい * map、flatMap、foreach、withFilterがあればよい、というのは、ライブラリ提供側や実装観点からしたら楽(なかったらforで呼べないだけ)

なんでも型でがんじがらめにするのではなく。ある程度言語のコンパイラ都合に合わせるってのもありなんだなと思いました。

*1:C#命名規則に合わせて、Camel形式で記載。ScalaだとmapとflatMapに相当