re:僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。
元ネタ: 僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。 - 亀岡的プログラマ日記
OOPの文脈で見ると、元の記事が言っていることもわからなくはないのですが、対象が広すぎていろいろと不正確になってしまっているので、ちょっとまとめてみます。
元の記事が対象にしているのは、Maybe / Optional / Nullableの3つです。 対応する型を持つ言語を見てみると、下記のようになります。
これらは、「値がないこと」を表すもの、という見方では同じですが、それぞれ異なる価値観の元に作られています。
Maybe/OptionalとNullable
これらはすべて型パラメータを取ります*1。
しかし、この中でNullable
だけは型パラメータに値型のみという制約が付きます。
これは、C#のNullable
は他のものとかなり性質が違うことを意味しています。
元の記事では並べて書かれていましたが、そもそもNullable
に関してはここに並べるべきものではありません。
C#のNullable
C#のNullable
は、値型にもnull
が使いたいという動機で導入された機能です。
そのため、Nullable
はMaybe
やOptional
と違って、むしろnull
を積極的に使えるようにするための機能と言えるでしょう。
これによってC#の上ではNullable
を使うことでコード上の見た目だけに関しては、値型でもnull
が使えるように見せています。
見た目だけの話であれば、SwiftのOptional
と同じように型名の後ろに?
が付くため、同じような言語機能に見えますが、実際には全くの別物と考えたほうがいいでしょう。
JavaのOptional
JavaのOptional
は、メソッドの戻り値として使うことが想定されているらしく、引数やフィールドに使うことは想定されていないそうです。
JavaのOptional
は参照型なので、Optional
自体がnull
になりうるという点を考えると、引数に使うべきではないというのもある程度納得できます*2。
Javaでは現状、型パラメータに指定できるのは参照型だけであり、null
になりうるのも参照型だけです。
そのため、JavaのOptional
はC#のNullable
と違って、(戻り値としての)null
を排除しよう(少なくとも、減らそう)という動機で導入されたとみていいでしょう。
SwiftのOptional
SwiftのOptional
は、Javaと名前は同じですが、より言語の仕組みに根差しています。
Javaでは、Optional<T>
型の変数にT
型の値を代入できません。
Integer i = 42; Optional<Integer> x = i; // コンパイルエラー
それに対して、Swiftではそれが可能です。
var i: Int = 42 var x: Optional<Int> = i // OK // Optional<Int>はInt?とも書ける
SwiftではOptional
ではない型にnull
*3は代入できません。
ですが、Optional
という仕組みによって今までの知識からあまり外れない書き方でnull
を扱えるようになっています。
HaskellのMaybe
HaskellのMaybe
は実装としてはSwiftのOptional
に一番近くなっています。
ただ、他の言語と違ってHaskellではそもそも他の言語のようなnull
はなく、Swiftのように既存言語ユーザーを取り込むために「今までの知識からあまり外れない書き方」というのも求めていません。
結果だけ見ると、JavaのOptional
と同じように、Maybe t
型の変数にt
型の値は代入できません。
Nullのダメな理由
元記事では、「Nullは何がダメなんだっけ?」について、
- Nullは型ではない
- あるメソッドがNullになるかどうかの判断ができない
が挙げられています。 細かい点ですが、ここにも誤解があります。
(少なくともJavaでは)Nullは型を持つ
まず、Javaの話であればnull
は型を持っています*4。
そのため、「ClassHoge
のnull
」という表現は、少なくともJavaにおいては正確ではなく、正確には「ClassHoge
型に暗黙変換されたnull
」とでもなるでしょうか。
詳しくは言語仕様を参照してください。
ここで言いたかったのはおそらく、「null
はどんな型にも変換できてしまう」ということでしょう。
あるメソッドの結果(もしくは変数)がNullかどうかの判断が型だけからできない
これも細かいですが、あるメソッド(の結果)がNullになるかどうかの判断ができないというのは、正確には「型を見ただけでは」できないのであり、元記事中にあるように、if
文によって実行時にはチェックできます。
本題
さて、ここからが本題です。
元記事では、Optional
が解決するのは2だけ、としていますが・・・
Javaの場合
JavaのOptional
では、Optional<T>
型とT
型は全くの別物であり、相互に暗黙変換できません。
そのため、
- (
null
に相当する)Optional.empty()
はOptional
型の変数にしか入れることはできない*5 - あるメソッドの結果が
Optional.empty()
になりうるかどうか型だけで判断できる
となります。
Swiftの場合
SwiftのOptional
は、Optional<T>
型の変数にT
型の値を代入できますが、その逆はできません。
そのため、
- (
null
に相当する)nil
はOptional
型の変数にしか入れることはできない - あるメソッドの結果が
nil
になりうるかどうか型だけで判断できる
となります。
Haskellの場合
HaskellのMaybe
は、Maybe t
型とt
型は全くの別物であり、相互に暗黙変換できません。
そのため、
- (
null
に相当する)Nothing
はMaybe
型の変数にしか入れることはできない - ある関数の結果が
Nothins
になりうるかどうか型だけで判断できる
となります。
元記事が言いたかったこと
このように、Optional
もしくはMaybe
は、1も2も解決できています。
これは十分なメリットであり、このメリットだけでもOptional/Maybeは有用です。
ただ、元記事を見ると、
本当に解決したいのは「Nullに型独自の処理をさせたい」なので、残ったままです
とあります。 つまり、本当に問題にしていたのは、「型Aの値がない場合と型Bの値がない場合で別の処理をさせたい」となります。
if文相当の処理を書く必要性
Optional/Maybeを使ったコードで頻繁にif
相当の処理を書いてしまっている場合、それはそもそもOptional/Maybeの使い方を間違っている可能性が大きいです。
リストを操作する関数にリストを返す関数が多く含まれるのと同じように、あるいはTask
を返す関数を内部で呼び出している関数がTask
を返す関数になるのと同じように、Optional
/Maybe
を返す関数を内部で呼び出している関数は、その関数の戻り値もOptional
になることがよくあります。
この場合、if
相当の処理を書くのは間違っています。
// Javaだと思う(Swiftは書いたことないので) Optional<User> user = findUser(pred); if (user.isPresent()) { // 何かする return Optional.of(何か処理の結果); } else { return Optional.empty(); }
例えば、「何かする」の部分がuser
の中身を使った関数を呼び出すような処理の場合、
// 値がなかった場合のことは気にしない return findUser(pred).map(user -> 何かする関数(user)); // ラムダ式を使わずに直接関数を渡す、でも可
のようになるでしょう。
map
の他にも、様々なメソッドが用意されているため、if
による分岐に頼る場面は少なくなります。
Haskellの場合、do
構文によってさらに複雑な例でもすっきり書けますが、それはまた別の話。
手続き的には見えない
さて、上のコードが手続き的に見えるでしょうか?
呼び出し側はOptional
のチェックを行っておらず、コードも短く簡潔です。
ではオブジェクト指向プログラミングではどのようになるでしょうか?
User
に対してNullObject
を用意して、このようになるでしょう。
public interface User { 戻り値の型 何かする処理(); } public class UserImpl implements User { @Override public 戻り値の型 何かする処理() { // 何かする return 何かした結果; } // ... } public class EmptyUser implements User { @Override public 戻り値の型 何かする処理() { return 戻り値の型のNullObject; } // ... }
// 呼び出し側は意識しない(意識できない) return findUser(pred).何かする処理();
呼び出し側だけ見ると、Optional
のときとそんなに変わってませんよね*6。
つまり、元のコードも手続き的には見えないと言っていいでしょう。
NullObjectをちゃんと使いたい、というよりは、Optionalをどう使うか考えたい
OOPの文脈で、どこまでOptional
を使えばいいのかというのはよくわかりません。
Optionalを使うと、どうしてもコードはOOPから離れて行ってしまうという感覚は確かにあります。
どのあたりでバランスを取るのがいいのかはケースバイケースでしょうし、一般化できるものではないと思っています。
コメント欄では id:kyon_mm が(やはりOOPの文脈で)
Optionを返していいのはむしろprivateやprotected的なものだけだと思っていて、それ以外はオブジェクトだったり、バリアント(判別共用体)的な感じで返すのが「オブジェクトとやり取りをしていて、それぞれに責務が割あたっている」といえるのではないだろうか。と思っています。
と言っています。
これはこれで一つの態度ではありますが、基本的なライブラリでは Optional
を返す関数*7というのはもっと積極的に作られることになると思います。
FPに軸足を置くか、OOPに軸足を置くかは、対象の性質やメンバーのスキル等によって決めていくしかないでしょう。 ただし、間違ったOptionalの使い方をもって「しっくりこない」というのであれば、まずはOptionalの正しい使い方を学び、実践すべきです。 そのための言語としては、F#なんていかがでしょうか。