はじめに
SwiftのOptionalを取り巻くあれやこれやについて考えを整理しつつ書きます。
多くの人にとっては役に立たない雑多な話題かもしれません。差し当たりSwift EvolutionのDraftのDraftぐらいの話でしょう。つまりほぼ日記です。
ImplicitlyUnwrappedOptional T! については話がややこしくなるので割愛します。
SwiftのOptionalの特徴についてまとめる
定義
SwiftにおけるOptionalは以下の定義です。
enum Optional<T> {
case some(T)
case none
}
associatedvalueとして任意の型のTを持つ場合someと、持たない場合noneが切り分けられています。
Optionalが存在することで、SwiftはOptional以外の型がnull値を持つことを許していません。一般にnullセーフと呼ばれるプログラミングが可能になります。
https://qiita.com/koher/items/e4835bd429b88809ab33
T??
OptionalにOptionalを持たせるとネストした型として表現できます。例えばT??であれば以下の3パターンを区別することが出来ます。
let value: Int?? = 1
switch value {
case .none: print("case1")
case .some(.none): print("case2")
case .some(.some(let value)): print("case3")
}
例えばOptionalの配列について、firstの結果がnilであった場合に、
- 配列自体が空だった→
.none - 配列の一番目が
.noneだった→.some(.none)
の区別をつけることが出来ます。
共変性(covariance)
Swiftではコンパイラによる特別扱いによって、Optionalについて以下の2つの共変性がある程度認められています。
-
TisT? - if
UisTthenU?isT?
T is T?
例えばInt?で宣言されたフィールドや引数に直接1やInt型の変数を渡すことが可能です。
またある値を返す関数オブジェクトを、Optionalの値を返す関数オブジェクトとして暗黙的に変換して渡すことが出来ます。
func foo() -> Int { return 1 }
let fooOptional: () -> Int? = foo
if U is T then U? is T?
Swiftではstandard libralyの幾つかの型についてこの共変性が認められています。
代表的なものはOptional, Array, Dictionaryです。
例えばAnimalとCat、2つのクラスが継承関係にあるなら、それらのOptionalもまた同様の関係として扱えます。
class Animal {}
class Cat: Animal {}
let animalOptional: Animal? = Cat?.some(Cat())
これは1.との組み合わせも可能で、[Animal]は[Animal?]の変数に直接代入出来ます。
「ある程度」認められている
勿論サブタイプの関係ではないので、型でチェックするとfalseと帰ってきます。
T.self is T?.Type // false
クイズ
突然ですがクイズです。以下の関数の返す値を予想してください。答えはPlaygroundで確かめてみてください。
func q() -> Bool {
let v1: Int?? = nil
let v2: Int? = nil
return v1 == v2
}
ConditionalConformanceが使える環境であればそのまま実行出来ます(そちらを推奨します)が、Swift4.0.2では出来ないので、以下の関数も実装してください。
func == <T: Equatable>(lhs: T??, rhs: T??) -> Bool {
switch (lhs, rhs) {
case (.some(.some(let l)), .some(.some(let r))): return l == r
case (.some(.none), .some(.none)), (.none, .none): return true
default: return false
}
}
さて、結果は予想通りでしたでしょうか?
問題なのは、正解したか不正解したかではなく、この結果がどちらであるかという予想が、どちらも妥当であるという点です。
恐らく正解、不正解の予想は半々になると思っています。
解説
関数qにおけるv1とv2の比較において、Int?からInt??への変換が発生します。この時に何が起こるべきか、という判断で結果が変わります。
TからT?への変換が発生するはずだ
Int?をTに当てはめて考えます。この場合は.someでラップすればいいので、v2は.some(.none)になります。
v1は.noneなので結果はfalseになるはずですね。
IntはInt?なのでU?からT?への変換が発生するはずだ
IntはそのままInt?として扱えます。ネストしたOptionalだとややこしいので一旦配列で考えてみましょう。
[1, 2, 3]を[Int?]に代入すると、[.some(1), .some(2), .some(3)]になります。
値がなければ、[]はそのまま[]になりますね。
ということはOptionalでもそのまま.noneになる、v2は.none、v1と等しいので結果はtrueになるはずですね。
じゃ、実際どうなの
結論から言うと、ネストした型の内部で何が起きるかは「わからない」です。どっちにも転びます。コンパイラの気持ちになりましょう。(無理)
一応は報告済みですが、問題の中枢が根深いのでノータッチなんだと思います。
https://bugs.swift.org/browse/SR-6126
flatMap abusing
クイズは終わり、全く別の話題です。
最近通ったEvolutionの一つに、ArrayのOptional版flatMapをリネームしようぜと言うものがありました。
https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md
何が問題だったかというと、(T) -> Uの関数オブジェクトを(T) -> U?として変換できてしまうがために、本来mapを使うべきシーンであっても、flatMapが使えてしまう、というものでした。
事実上flatMapがPromiseにおけるthen相当のものになっていて、それはもう由々しき事態、当然リネームするに至ったわけでした。
Arrayはリネームで解決した格好ですが、[T]とT?をオーバーロードして別名でTを使いたいというユースケースは普通にあって
https://github.com/ReactiveX/RxSwift/blob/e1933ebe219e89021f6ffd4065cfc4fef2c002c7/RxSwift/Deprecated.swift#L9-L36
依然として困った状況は続いています。
[T]とT?をオーバーロードしたい理由は、これらがmap/flatMapを持っている所謂一般にモナドと呼ばれる類の構造だからです。まとめて取り扱うと捗るシーンが多いんですよね。
どうすれば良いのか
ここからは僕のTodoです。Evolutionを出さなければいけません。(他の人が出してくれると僕は楽を出来て幸せです)
T is T?がまずいだろう、という事はわかりやすいのですが、これをコンパイラから消去するとなると影響範囲が無限に大きいので採択はされないでしょう。
誰もが幸せになれないとしても、得られる幸福を最大化しつつ不幸を少なくした提案をしなければいけません。
妥当だろうと思っているのが、ジェネリクスパラメータについては共変性を禁止する、関数オブジェクトについては共変性を禁止するか、警告+明示的変換は無警告、その他変数については、警告+明示的変換は無警告、或いはOptional wrapping operatorを提案するなどを考えていますが、いずれにしてもPRも作らないとEvolutionは提案出来なくなってしまったので、(これはどのタイミングなのかはよくわかっていない)コンパイラのコードと英語とで腐心する毎日です。