Scalaへの理解を深めるために、
C#でOption相当のクラスを実装してみよう
と思い立ちました。
- Optionそのものの定義
- Optionの各種メソッド
- SomeとNoneの定義
- Scalaの実装
- 唐突に答えが分かる
- そもそもの勘違い
- 自分の常識を疑うことも大事
- (もはや余談)C#で実装したOptionを使う
Optionそのものの定義
C#では、Scalaと違って共変と反変はinterface
とdelegate
にしか付けられないという制約があるので、
public interface IOption<out A> { bool IsEmpty { get; } A Get(); }
という定義にして、共変制約を付けています。なお、先頭にI
を付けないと命名の警告が出るので、IOption
という名前で妥協しています。
Optionの各種メソッド
そして、拡張メソッドで各種メソッドを生やします。
public static class Option { public static IOption<A> None<A>() { return new None<A>(); } public static IOption<A> Some<A>(A value) { return value == null ? throw new ArgumentNullException("") : new Some<A>(value); } public static IOption<A> Create<A>(A value) { return value == null ? None<A>() : new Some<A>(value); } public static B GetOrElse<A, B>(this IOption<A> self, Func<B> def) where A : B { return self.IsEmpty ? def() : self.Get(); } public static IOption<B> Map<A, B>(this IOption<A> opt, Func<A, B> f) { return opt.IsEmpty ? None<B>() : new Some<B>(f(opt.Get())); } public static IOption<B> FlatMap<A, B>(this IOption<A> opt, Func<A, IOption<B>> f) { return opt.IsEmpty ? None<B>() : f(opt.Get()); } public static void ForEach<A>(this IOption<A> opt, Action<A> f) { if (!opt.IsEmpty) { f(opt.Get()); } } }
Option
の全メソッドは面倒だし、withFilter
以外はすんなり実装できるので、省略しています*1。
なお、Some
もNone
もただのクラスになるのですが、利用先で毎回newするのは面倒なので、上記Option
クラスにショートカット用メソッドを用意しています。
SomeとNoneの定義
次に、Some
*2とNone
を定義します。IOption<T>
を実装する形です。
internal sealed class Some<A> : IOption<A> { private readonly A _v; public Some(A value) => _v = value; public bool IsEmpty => false; public A Get() { return _v; } } internal sealed class None<A> : IOption<A> { public bool IsEmpty => true; public A Get() { throw new InvalidOperationException("object is not exist."); } }
さて、None
の定義がScalaの場合と違いますね。None
に型制約がついてしまっています。
Scalaの実装
Scalaの場合、Some
とNone
の定義は以下のようになっています。コメント等は除去しています。
final case class Some[+A](value: A) extends Option[A] { def isEmpty = false def get = value } case object None extends Option[Nothing] { def isEmpty = true def get = throw new NoSuchElementException("None.get") }
Some
に共変がついているのは良いとして、問題はNone
です。C#ではNone<A>
と型制約を付けなければならないのに、Scalaでは不要です。
Scalaでなぜ型制約をつけなくてよいのか、そしてC#で型制約なしのNone
が実現できないか、かなり真剣に悩みました。
なんとなく、ScalaのOptionをC#で実装したら理解深まるかな、と思ってやってみたけど、Noneの表現が難しい…
— ひろ (@E2piro) June 29, 2019
ScalaのNoneって、Option[String]の関数の戻り値として返せるけど、C#はそのままだと返せない(どう妥協してもNone<string>になる)
つぶやきました。すると、Scala界隈ですごく有名な@kmizu
さんから返信いただきました。
真似するにはNothing相当のbottom typeが必要そうな気がします。
— Kota Mizushima (@kmizu) June 29, 2019
このときは、
Nothing相当のbottom type
の意味が分かりませんでした。ScalaのNothing
= C#のobject
みたいな感じかな、みたいに考えていました。
そして一週間、どうにかしてC#でScalaのNoneを表現できないか、試行錯誤しました。case class
だからじゃね?とかいろいろ考えましたが、どれも釈然とせず。
唐突に答えが分かる
この記事を書く直前です。
そもそもNothingってなんだよという疑問が、ようやく湧きました。遅い。頭が固い。
そして@kmizu
さんの10年ぐらい前の記事に辿り着く。
そして気づきます。Nothing
は継承元じゃなくて継承先だわ、と。スーパータイプじゃなくてサブタイプだわ、と。
そもそもの勘違い
C#では、「全ての型の基底型」としてobject
が存在します。そして、値型の継承元としてValueType
が存在します。あとはdelegate
も型として扱われます*3。そして、すべてのクラスはこれらを継承元として派生します。
これに囚われていて、型システムは、基底型しか提供してこないみたいな先入観を持っていました。
そして、改めて公式ページを見ます。
気づきます。
NothingとNullの位置おかしくね?
と。暗黙的に全ての型のサブタイプになるとかできるんだ…って思いました。これすごくね?と。
例外はNothingを返す
というのは目から鱗です。たしかにそうすれば、例外を特殊扱いしなくて済みます。
以下のC#のソースは、例外を投げているゆえに戻り値は返さないように見えます。型システム上安全である、というより、「例外が投げられたから特殊扱いしました」みたいに見えます。
public A Get() { throw new InvalidOperationException("object is not exist."); }
以下のScalaのこのソースは、「例外はNothing。bottom typeだから戻り値としては正しい(だけど例外だから特殊処理しましょうね)」というように見えます。
def get = throw new NoSuchElementException("None.get")
似ていますが、これは意味が全然違います。ここまで考えてNothng
が用意されているのを知って、本当に作った人は天才だと思いました。Scala美しい…すごい。
もしかしたらC#の例外も内部実装上はScalaと同じかもしれませんが、そこまで調べられませんでした。
自分の常識を疑うことも大事
初めてScalaを勉強したとき、型システムを見て、だいたいC#と同じかと思い、気にすることなく通り過ぎていました。
言語としては継承元だけ用意していると思い込んでいましたし、それで支障が出ることもありませんでした。
共通の継承先を用意するなんてことが実現できているなんて、考えもしませんでした。自分の常識では「そんなことはあり得ない」みたいに思っていましたし。
今回、ちゃんと型システムを見直す機会をたまたま作り、個人的には腑に落ちる形で結論が出せました。
自分の常識が実は常識じゃなかったり、またその逆もあるのだと、思い知った出来事でした。
(もはや余談)C#で実装したOptionを使う
サンプルです。というか適当な実装です。
var someOpt = Option.Some("hiro"); // flatmap someOpt .FlatMap(s => DummyLogin(s)) .Map(s => new User(s)) .ForEach(user => Console.WriteLine($"Hello { user.Name }"));
結論を言うと、実装はできるし、便利には使えるものの、for式があるScalaには及ばないという感じでした。でも使えそうではあります。
非同期になったら途端に使いづらくなりそうです。まあ匿名async使えばいけるかな…?微妙かな?