前回、この記事を書きまして。
とりあえず、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に呟いたらコメントいただきました。
言語機構としてLINQは内包表記として扱うことができるので、実は本質的にfor-yieldと同じことができます
— がくぞ (@gakuzzzz) July 19, 2019
□Scala
val value = for {
a <- opt1
b <- opt2
} yield a + b
□C#
var value =
from a in opt1
from b in opt2
select a + b
なんだと…?
C#でLINQを使う場合、以前から拡張メソッド方式に慣れるようにしていたせいで、クエリ式の存在を完全に忘れていました。
これなら、LINQクエリ式が要求するシグネチャのメソッドで、Optionの関数を呼び出してやれば、C#でもOptionっぽいの作れるやん!と思ったので、試してみましょう。
なお、実装しながら記事を書くので、半端なところで終わったらごめんなさい。
前提知識
記事はおそらく長くなるので、前提知識を説明せずに丸投げします。
C#のLINQで使われるクエリ演算子(クエリ式)
ScalaのOption
実装していく
名前空間の考え方
上で触れたとおり、クエリ式で書けるようにするためには、LINQのメソッド名に合わせたメソッド名や型定義での実装を提供する必要があります。
一方、関数型のような名前、MapやFlatMap*1というメソッドも用意したいです。そこで、Functional
という名前空間にMapなどのメソッドを用意し、Linq
という名前空間にLINQ仕様のメソッドを用意します。
名前空間 | 役割 |
---|---|
FunFun.Data |
Optionの型定義と生成処理 |
FunFun.Data.Functional |
関数型ライクな命名を行ったメソッド。実装はこちらに書く |
FunFun.Data.Linq |
LINQに合わせた実装を提供する。できる限りFunFun.Data.Functional のメソッドを呼ぶようにする |
ScalaのOption
で定義されている関数は、拡張メソッドとして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); } } }
Some
とNone
の実装は、公開しても扱いづらいだけなので隠蔽しています。
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);
のように書けるところを目指します。
ちゃちゃっとテストを準備します。すると、
となり、クエリ式(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の中でもっとも単純なタイプです。
必要な定義は以下の通りです。
public static IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);
これの、IEnumerable
をIOption
に置き換えたものが必要となります。
とはいえ、やっていることは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
を追加すると、
コンパイルが通り、テストに成功するようになります。素晴らしい。
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); }
今は通りません。
今度は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実装
作ります。今回も最も単純なパターンをまず用意します。
必要な定義は以下の通りです。
SelectMany<TSource,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TResult>>)
これの、IEnumerable
をIOption
に置き換えたものが必要となります。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); } } }
しかし、テストが通りません。ビルドエラーになります。
実装すべきオーバーロードが異なるようです。なので、別のを実装します。
SelectMany実装 その2
必要なのはこちらです。
SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TCollection>>, Func<TSource,TCollection,TResult>)
こんな感じで実装されています。
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に上げたので、追加していきます。
まとめ
C#視点からだと、
- C#のクエリ式は、いろんなリソースを同じように扱いたいという思想だが、これが意外と便利
- 名前が合っていればよい、というDuck Typing的な考え方だったことを初めて知った
- 自分でクエリ式に対応する場合、対応するLINQメソッドのシグニチャに合わせていけばいい(どれに合わせるか、資料がどこかにあるはず)
- ScalaのOptionぐらいなら、意外と実装できるのではないか。使い勝手自体はScalaのforみたいな感じになる
- これを実装したらLINQで呼べるよ、というようなinterfaceを用意しなかったのは偉い。必要な分だけ実装すればよいため、LINQ準拠のライブラリ提供が楽になっている(今回だと、Optionはシーケンスではないため、シーケンス関連のメソッドは実装を提供する必要が無い)
Scala視点からだと、 * forを標準で用意したのはすごい * map、flatMap、foreach、withFilterがあればよい、というのは、ライブラリ提供側や実装観点からしたら楽(なかったらforで呼べないだけ)
なんでも型でがんじがらめにするのではなく。ある程度言語のコンパイラ都合に合わせるってのもありなんだなと思いました。