はじめに
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つの共変性がある程度認められています。
-
T
isT?
- if
U
isT
thenU?
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は提案出来なくなってしまったので、(これはどのタイミングなのかはよくわかっていない)コンパイラのコードと英語とで腐心する毎日です。