本投稿の個別の説明( Optional とは何か、 ?. と map, flatMap の関係、その背後にあるモナドという概念)は 2017 年現在でも通用するものですが、 Swift の Optional の使い方としては、僕の考えとのズレが大きくなってきました。 Swift の Optional をいつ・どのように使うべきかについてもまとめた ので、そちらも併せて御覧下さい。
Optional は Swift の仕様の中でもっとも素晴らしいものの一つだと、僕は考えています。
null参照 (多くの言語で nil や null と呼ばれるもの)を発明したトニー・ホーアは次のように述べています1。
それは10億ドルにも相当する私の誤りだ。null参照を発明したのは1965年のことだった。(中略)これは後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。
その「10億ドルの誤り」からプログラマを開放してくれるのが Optional です。
しかし、実際に Optional を使い始めると、コードを書きづらいパターンがいくつかあることに気付きます。本投稿では、 Optional の背景にある概念について説明し、そのようなケースのエレガントな解決法を示します。
本投稿の主なキーワード
-
map,flatMap - 第一級関数, 高階関数, 高階メソッド
- モナド
- Union type
Optionalで書きづらいケース
次のようなケース(先日「力試し」として投稿したものです)では、 Optional binding ( if let ... )などで書こうとするとコードが複雑になってしまいます。
Optionalを使って次のことをやる場合に、 簡潔かつ安全 に書く方法を考えて下さい。
a: Int?があるとき、aの値を二乗したい。ただし、aがnilの場合はnilを得たい。array: [Int]?があるとき、arrayの最初の要素を取得したい。ただし、arrayがnilか、最初の要素が存在しない(Arrayが空の)場合はnilを得たい。(ヒント:[Int]はfirst: Int?というプロパティを持つ。firstは最初の要素が存在しない場合はnilを返す。)x: Double?があるとき、xの平方根を計算したい。ただし、xがnilまたは負の数の場合はnilを得たい。なお、sqrtを安全にした(負の数を渡すとnilを返す)関数safeSqrt: Double -> Double?があるものとして考えて良い。a: Int?とb: Int?があるとき、a + bを計算したい。ただし、aかbがnilの場合にはnilを得たい。a: Int?があるとき、aの値をaがnilの場合には何も表示したくない。1, 2, 4 では結果の型が(
Intではなく)Int?に、 3 ではDouble?になることに注意して下さい。また、 3 のsafeSqrtは次のように実装できます。
func safeSqrt(x: Double) -> Double? {
return x < 0.0 ? nil : sqrt(x)
}
例えば、 1 の答えを _Optional binding_ を使って書くと次のようになります。
```swift
let result: Int?
if let a0 = a {
result = a0 * a0
} else {
result = nil
}
ただ値を二乗したいだけなのに複雑すぎます。もし a が Int? ではなく Int なら
a * a で済むコードが 6 行にもなってしまっています。
別の方法として、 nil チェックと三項演算子、 Forced unwrapping ( ! )を組み合わせれば 1 行で次のように書くことはできます。
let result: Int? = a == nil ? nil : a! * a!
しかし、 ! は安全ではないので極力使いたくないですし、短いだけで簡潔とも言いづらいです。そもそも、 nil チェックが必要なんて null参照 時代に逆戻りでせっかくの Optional が台無しです。
Optionalの考え方
解答の前に、まずはどのようなイメージで Optional をとらえればよいか説明します。
Optional を nil も入れられる型 だと考えている人は多いと思います。よくあるのは次のような理解でしょうか。
-
Foo型の変数にはnilを代入できないけど、Foo?にすればnilも入れられる。 -
Foo?型の変数はそのままでは使えないので Optional binding (if let = ...)や Forced unwrapping (!)を使ってFoo型に変換してから使う。
これらは間違ってはいないですが、 Optional の正確な理解とは言えません。
Foo? は Optional<Foo> と等価
まず、 Swift の Optional は Optional という一つの型です。 Foo? は Optional<Foo> を表すシンタックスシュガーで、どちらで書いても同じ意味になります。
let a: Optional<Int> = 3 // Int? と書いたのと等価
これは、 [Foo] が Array<Foo> と等価なのと同じです。
Foo? が Foo のメソッドを呼べない理由
次のコードがコンパイルエラーになるのは当然ですよね?
let a: Array<Int> = [3]
let b = a * a // コンパイルエラー: Array に対して * が呼び出せるわけがない
Optional についても全く同じ理由でエラーになります。
let a: Optional<Int> = 3
let b = a * a // コンパイルエラー: Optional に対して * が呼び出せるわけがない
つまり、 Foo? を Foo として使えないのは Foo? が nil かもしれないからではなく、 Foo? ( Optional<Foo> )は Foo とはまったく異なる型だからです。 String を Int として使えないのと同じ話です。
なお、 Optional<Foo> は Foo のメソッドを呼ぶことはできませんが Optional のメソッドを呼ぶことはできます。 nil は Optional 型の値なので2 nil に対して Optional のメソッドを呼び出すことも可能です。
let a: Optional<Int> = nil
a.getMirror() // Optional<Int> の持つ getMirror メソッドを呼ぶ → エラーにならない
Foo? を nil も入れられる型だと考えていたら、なんで nil に対してメソッドを呼ぶことができるのか理解しづらいと思います。 Swift の nil は Optional 型の値2であって null参照 とはまったく別の概念です。
Optional について混乱したときには、 Foo? ではなく Optional<Foo> だと考えてみることをオススメします。ただそれだけのことで考えを整理しやすくなると思います。
Optionalは箱と考える
Foo? を nil も入れられる型だと考えないのであれば、どのようなイメージでとらえると良いでしょうか。僕は、 Foo? は Foo 型の値を入れられる箱だと考える とわかりやすいと思います。
箱の中身は空( nil )かもしれないし、値が入っているかもしれません。値を使うには箱から取り出さなければ( unwrap しなければ)いけません。
このイメージの違いは些細なことに感じられるかもしれませんが、箱というより正確なメタファーで与えることで、次のような複雑なケースもシンプルに理解することができます。
let a: Int?? = 3
Optional を nil も入れられる型だと考えていると Int?? を理解しづらいです。しかし、 Optional を箱だと考えると、 Int?? ( Optional<Optional<Int>> )は箱が二重になっており、内側の箱の中に Int が入ってるとイメージできます。箱が二重になっているなら、 箱を二回開けて中身を取り出さないと中の値を使えないことがすぐにわかります。
let a: Int?? = 3
if let a0: Int? = a { // 外の箱を開けて内の箱を取り出す
if let a1: Int = a0 { // 内の箱を開けて値を取り出す
println(a1) // 3
}
}
箱から取り出さずに中身を操作する
Optional は null参照 と違って安全で素晴らしいですが、 Optional を使った安全なコードを書いているとすぐに箱( Optional )だらけになってしまいます。そして、「力試し」の 1 (下に再掲)のように箱の中身をちょっと操作したいと思っただけで、わざわざ箱から値を取り出して操作してもう一度箱に詰めるというわずらわしい作業が必要になります。
1.
a: Int?があるとき、aの値を二乗したい。ただし、aがnilの場合はnilを得たい。
できることなら、箱から中身を取り出さずに箱の中身だけ操作できれば簡単です。 Optional には、そのための道具 map メソッドが用意されています。
let result: Int? = a.map { $0 * $0 } // 箱が空でなければ中身を二乗する
とてもシンプルですね!
しかし、上記のコードを見てもどういう構文なのかよくわからないという人も多いかと思います。これを理解するには次の各ステップを理解する必要があります。
第一級関数
Swift では関数を、
- 変数に代入する
- 関数やメソッドの引数として渡す
- 関数やメソッドの戻り値として返す
ことができます。このことを、「 Swift の関数は 第一級関数 である」と言います。
func square(x: Int) -> Int { // 引数を二乗して返す関数
return x * x
}
// 変数に関数を代入
let sq: Int -> Int = square // sq は「 Int を受けて Int を返す関数」型の変数
// 変数 sq に代入された関数を呼び出す
let result: Int = sq(3) // 9
高階関数
関数を引数に受けたり、戻り値として返したりする関数のことを 高階関数 (メソッドの場合は 高階メソッド )と呼びます。
// 渡された x を関数 f に適用して println する高階関数
func applyAndPrint(x: Int, f: Int -> Int) {
println(f(x))
}
applyAndPrint(3, square) // 9 ( 3 に square が適用される)
mapメソッド
Optional<T> の map メソッドは次のような 高階メソッド として宣言されています。
func map<U>(f: T -> U) -> U?
やや複雑ですが、これは「 T を受け取って U を返す関数」を引数に受け取るということです。 map メソッドに、さっきの関数 square を渡してみましょう。
let a: Int? = 3
a.map(square) // map メソッドの引数 f に関数 square を渡す → 結果は Optional(9)
この場合、 a の型は Optional<Int> 、 square の型は Int -> Int なので map メソッドの型は次のように解釈されます。
func map(f: Int -> Int) -> Int?
Optional の map メソッドは次のような挙動をします。
- もし箱の中身が空(
nil)だったら空の箱(nil)を返す。 - 箱に値が入っていたら、その値を渡された関数
fに適用する。そして、適用した結果を新しい箱に詰めて返す。
先程の square の例では、 a の箱には 3 が入っているので、 square に 3 が渡され 9 が得られます。そして、それが Optional に包まれて return されます。
クロージャ
関数 square と map メソッドを使えば、箱から値を取り出さずに中身を二乗することができました。しかし、箱の中を操作する度にわざわざ関数を作るのは面倒です。 Swift には関数をその場で記述する構文 クロージャ があります3。
例えば、 func を使って square を定義する代わりに、 クロージャ を使って次のように書くことができます。
let square: Int -> Int = { (x: Int) -> Int in
return x * x
}
クロージャ のブロック内に式が一つだけ書かれている場合には、その式の結果が自動的に戻り値として使われます。そのため、 return を省略して次のように書けます。
let square: Int -> Int = { (x: Int) -> Int in x * x }
また、 クロージャ の宣言にも型推論が働くので、型宣言を省略して次のように書けます。
let square: Int -> Int = { x in x * x }
さらに、引数名を省略することもできます。引数名を省略した場合は、第一引数が $0 、第二引数が $1 、…となります。
let square: Int -> Int = { $0 * $0 }
map メソッドの引数に直接 クロージャ を書くと次のようになります。
let result: Int? = a.map({ $0 * $0 })
Trailing Closure
Swift では、関数やメソッドの引数として クロージャ を渡す場合、最後の引数については クロージャ を外出しして if や for のブロックのように書くことができます。
let result: Int? = a.map() { $0 * $0 }
また、その結果 () の中に書く引数がなくなった場合には、 () も省略することができます。
let result: Int? = a.map { $0 * $0 }
これで、本節冒頭と同じシンプルなコードが得られました。
Optionalの考え方 (2)
次の問題に進む前に、 Optional の別の側面について説明します。
Optionalを使って失敗を表す
String の toInt メソッドは String を Int に変換するメソッドです。しかし、 "abc" のような文字列は Int としてパースすることができないので失敗します。失敗時には Int の代わりに nil が返されます。そのため、 toInt の戻り値の型は Int ではなく Int? になります。このことは大したことに思えないかもしれませんがとても重要です。
従来の null参照 の言語でも、関数やメソッドが処理に失敗したときに nil や null を返すことはよくあります。 Objective-C のライブラリはそのように設計されていますし、例外機構を持つ Java なんかでも失敗時に(例外ではなく) null が返ってくることも多いです。
Swift で nil が返された場合にそれらの言語と決定的に異なっているのは、 Optional は値を使う前に必ず nil かどうかチェックしなければならない ことです。
let numberOrNil: Int? = string.toInt()
if let number = numberOrNil { // そのままでは使えないので分岐
// パースに成功したときの処理
} else {
// パースに失敗したときの処理
}
Objective-C や Java では nil や null を返すメソッドに対して、失敗した場合の処理を無視しても問題なくコンパイルできます。そのため、意図しないエラー処理忘れが頻発し、多くのバグを生み出してきました。 null参照 を排除して Optional を導入することで、プログラマにエラー処理を強制し、そのようなバグを未然に防ぐことができるのです。
-
Optional は単に
nilを入れられるというだけでなく 失敗かもしれない値を表す ことがある。 - 失敗かもしれない値を Optional で表すことで エラー処理を強制することができる。
上記の二点を意識して、 Optional を見たときには失敗とエラー処理を意図しているのかもしれないという視点で見てみましょう。
箱の中身を操作する(失敗するかもしれない場合)
2.
array: [Int]?があるとき、arrayの最初の要素を取得したい。ただし、arrayがnilか、最初の要素が存在しない(Arrayが空の)場合はnilを得たい。(ヒント:[Int]はfirst: Int?というプロパティを持つ。firstは最初の要素が存在しない場合はnilを返す。)
この問題が 1 と違うのは、箱の中身に対する処理が失敗するかもしれないことです。もしこれを map を使って書こうとすると、次のように得られる結果が二重の箱( Int?? )になってしまいます。
// first の型は Int?
// map は値を Optional で包みなおして返すので Optional が二重になる
let result: Int?? = array.map { $0.first }
Swift には、このようなケースを扱うための構文 Optional chaining があります。これを使えば 2 の答えは簡単に書けます。
let result: Int? = array?.first
しかし、 3 のようなケースでは Optional chaining でうまく書くことができません。
3.
x: Double?があるとき、xの平方根を計算したい。ただし、xがnilまたは負の数の場合はnilを得たい。なお、sqrtを安全にした(負の数を渡すとnilを返す)関数safeSqrt: Double -> Double?があるものとして考えて良い。
2 と 3 は、箱の中身に対して失敗するかもしれない処理をしたいという意味では同じです。しかし、 Optional chaining は、箱の中身に対してメソッドをコールすることはできても、箱の中身を引数に渡すことはできません。
とりあえず、箱が二重になってしまいますが map で書いてみると次のようになります。
let result: Int?? = x.map { safeSqrt($0) } // 箱が二重になる以外はうまく動く
今、二重の箱が邪魔なので外側の箱だけ開けたいです。でも ! で開けるわけにはいきません。もし外側の箱が空( nil )だったらクラッシュしてしまうからです。
そこで ?? を使います。 ?? は ! 同様に箱を空けますが、箱が空だった場合には代わりの値で代用します。 a ?? b と書くと、 a が nil でなければ a の箱の中身を、 nil だった場合には b を返します。
// 結果の型が Int? ではなく Int なことに注意
let result1: Int = "2".toInt() ?? -1 // 2
let result2: Int = "X".toInt() ?? -1 // -1
?? は箱を一つ開けるので、 ?? を使えば二重の箱を一重にすることができます。
// 結果の型が Int?? ではなく Int? なことに注意
let result: Int? = x.map { safeSqrt($0) } ?? nil
flatMap
このような処理( map の結果の二重の箱を一重にしたい )はよく行われるので flatMap というメソッドが用意されています4。
flatMap を使えば 3 の解答は次のように書けます。
a.flatMap { safeSqrt($0) }
よりシンプルになりました。しかし、実はもっとシンプルに書くこともできます。
a.flatMap(safeSqrt)
map { ... } や flatMap { ... } の書き方に慣れてくるとどんな場合でもクロージャで { ... } と書いてしまいがちですが、箱の中身に適用したい関数がすでに存在しているなら、単にその関数を渡せば良いのです5。高階関数の説明で a.map(square) と書いたのと同じです。
flatMapの役割
Optional の flatMap は失敗するかもしれない連続した処理を書く場合に役に立ちます。
Optional chaining は同じ目的で使われますが、 3 の解答で見たように flatMap は Optional chaining でうまく書けないケースにも対処できます。逆に Optional chaining で書けるケースはすべて flatMap で書くことができます。
// 次の二つは等価
foo()?.bar()?.baz()?.qux() // foo, bar, baz, qux のどこかで失敗したら nil が返る
foo().flatMap { $0.bar() }.flatMap { $0.baz() }.flatMap { $0.qux() }
// Optional chaining では書けないケース
foo().flatMap { bar($0 * $0) }.flatMap { baz($0 + 1) }
Optional chaining は flatMap の一部のケースを簡単に書くためのシンタックスシュガー だと考えておくとわかりやすいです。
複数の箱の中身を使う
4.
a: Int?とb: Int?があるとき、a + bを計算したい。ただし、aかbがnilの場合にはnilを得たい。
この問題を解くには、二つの箱の中身を使って計算をする必要があります。箱を空けずに中身を使いたいので map で書いてみましょう。
let result: Int?? = a.map { a0 in b.map { b0 in a0 + b0 } }
二つの箱の中身を取り出すために map を入れ子にして使っています。この場合は 2 や 3 とは違って処理(足し算)は失敗はしませんが、 map を二重の入れ子にしたために結果も二重の箱( Int?? )になってしまっています。
flatMap
二重の箱をフラット(一重)にするために、ここでも flatMap が役に立ちます。
let result: Int? = a.flatMap { a0 in b.map { b0 in a0 + b0 } }
今、入れ子の内側が flatMap ではなく map になっていますが、これは a0 + b0 が失敗しない処理だからです。失敗する処理をしたい場合には内側の map も flatMap でなければなりません。
例えば、 safeSqrt と同じように安全な func safePow(x: Double, y: Double) -> Double? を考えます6。その場合、 a: Double? と b: Double? に対して safePow を計算するには次のようにします。
a.flatMap { a0 in b.flatMap { b0 in safePow(a0, b0) } }
より多くの Optional を使って何らかの処理を行いたい場合は、その分だけ flatMap を入れ子にすれば良いです。
// a, b, c が Int?
a.flatMap { a0 in b.flatMap { b0 in c.map { c0 in a0 + b0 * c0 } } }
アプリカティブスタイル
flatMap の入れ子はやや複雑に思えるかもしれません。 アプリカティブスタイル を使うとよりシンプルに書くことができます。
let result: Int? = (+) <%> a <*> b
アプリカティブスタイル については本題から外れてしまうのでここでは説明しません。アプリカティブスタイルの使い方については "まだSwiftyJSONで消耗してるの?#アプリカティブスタイル" に、実装の仕方については "Swiftでアプリカティブスタイル" により詳しい説明があります。 Swift でアプリカティブスタイルを使うには、 thoughtbot/Runes を使うのが一般的です。
モナドとしてのOptional
Swift の Optional は モナド です。
モナド とは何でしょうか。 モナド の定義7を読んでも圏論を知らないと理解するのは難しいです。プログラミングにおけるモナドについてはこちらで詳しく説明していますので御覧下さい。ここではその中から僕が一番しっくりきた説明8を簡単に紹介します。
モナドというのは、モナドでくるまれた中の世界ではモナドを気にせずに処理が記述でき、外からみるとモナドでくるまれているという、外と中を分離するための仕組みです。
Optional binding ( if let ... )や Forced Unwrapping ( ! )を使って Optional の中身を取り出すのは、分離されている箱の中の世界と外の世界をつなげてしまう行為です。 map や flatMap を使うことで、中と外を分離したまま中の世界に対する処理を記述することができる のです。
Promise と比べてみる
Optional だけを見て モナド をイメージするのは難しいので、他の モナド と比較してみましょう。ここでは Promise という モナド を取り上げます。
Promise は JavaScript でよく使われ、次期 JavaScript にも正式採用されたので知っている人も多いかもしれません。 Promise は主に非同期処理で使われ、今はまだないけど将来的に得られる値を表します。
例えば、サーバと通信して値を取得したいとします。しかし、サーバからのレスポンスを待っていると、その間 UI が固まってしまうなどの問題があります。通常はコールバックで結果を受け取るのですが、それだとその値はコールバックの中でしか使えず、取り回すことができないので不便です。また、連続した非同期処理を書くときには、コールバックのネストがどんどん深くなってしまいます。
Optional が 「中身(値)が空かもしれない箱」 なら、 Promise は、 「今はまだないけど将来的に中身(値)が得られる箱」 です。サーバ通信などの非同期処理を行う関数が、コールバックで結果を返す代わりに Promise という箱を返すことで、サーバレスポンスを待たずに結果を取り回すことができるようになります。
例を見てみましょう(ここでは、 Promise の then を map および flatMap として Swift で実装した PromiseK を使います)。
// asyncFoo: String -> Promise<Int> はサーバと通信して Int を取得する関数
let result: Promise<Int> = asyncFoo("abc").map { $0 * $0 } // 得られた結果を二乗
上記のコードでは、 asyncFoo はサーバと通信をして非同期的に結果を取得しますが、戻り値の Promise<Int> は即座に返されます。その Promise<Int> に対して map メソッドを呼び出すことで、「今はまだない値」を二乗しています。実際には map に渡された関数(ここでは引数を二乗する関数)はサーバから結果が取得できたあとに実行されます。コールバックのようですが、 map はまだ値が存在しない値を二乗した結果を表す箱( Promise オブジェクト)を返すことに注意して下さい。 Promise を使うことで、このように「今はまだない値」がまるで存在しているかのように扱うことができます。
では、 Promise のコードと Optional のコードを比べてみましょう。
// 非同期的に得られた結果を二乗
let a: Promise<Int> = asyncFoo("abc").map { $0 * $0 }
// 非同期的な処理を連続して行う( JS の Promise の then 相当)
let b: Promise<Int> = asyncFoo("xyz").flatMap { asyncBar($0 * $0) }.flatMap { asyncBaz($0 + 1) }
// 二つの非同期的な処理結果を利用した計算
let sum: Promise<Int> = a.flatMap { a0 in b.flatMap { b0 in Promise(a0 + b0) } }
// 失敗するかもしれない処理の結果を二乗
let a: Optional<Int> = failableFoo("abc").map { $0 * $0 }
// 失敗するかもしれない処理を連続して行う
let b: Optional<Int> = failableFoo("xyz").flatMap { failableBar($0 * $0) }.flatMap { failableBaz($0 + 1) }
// 二つの失敗するかもしれない処理結果を利用した計算
let sum: Optional <Int> = a.flatMap { a0 in b.flatMap { b0 in Optional(a0 + b0) } }
二つのコードは Promise と Optional が違うだけでそっくりですね! モナド という共通の性質を備えているから Promise と Optional は同じ方法で扱うことができる のです。
map や flatMap を使った書き方は一見難しそうに見えるかもしれません。しかし、一度慣れてしまえば、 Optional に限らず他の モナド も統一的な方法で扱うことができるようになり、便利でわかりやすいものです。是非 map と flatMap を習得して下さい。
その他のモナドの例としては、 Either について "Swift 3.0で追加されそうなEitherについて" を書いたので御覧下さい。
ArrayとOptional
ここでは、 Optional の別の顔について説明します。
実は Array を使えば Optional を表すことができます。 Optional は中身が空かもしれない箱でした。別の見方をすれば、 Optional を最大で一つしか要素を入れられない Array と考えることができます。
Optional を使った次のようなコードを考えます。
let a: Optional<Int> = 3
let b: Optional<Int> = a.map { $0 * $0 } // Optional(9)
let c: Optional<Int> = nil
let d: Optional<Int> = c.map { $0 * $0 } // nil
let sum: Optional<Int> = a.flatMap { a0 in b.flatMap { b0 in a0 + b0 } } // Optional(12)
これを Array で書き直してみましょう。
Array も モナド なので9、 map や flatMap を使うことができます。
let a: Array<Int> = [3]
let b: Array <Int> = a.map { $0 * $0 } // [9]
let c: Array<Int> = []
let d: Array <Int> = a.map { $0 * $0 } // []
let sum: Array <Int> = a.flatMap { a0 in b.flatMap { b0 in [a0 + b0] } } // [12]
このように考えると、 Optional はコレクションの仲間 とも考えられます。正規表現で ? が 0 回か 1 回の繰り返しを表すのに似ていますね!
箱の中身を使うけど結果は必要ないとき
5.
a: Int?があるとき、aの値をaがnilの場合には何も表示したくない。
この問題は map を使えば簡単に解けます。
a.map { print($0) }
このとき、 map の戻り値の型はどうなっているでしょうか? print の戻り値の型は Void なので、 map の戻り値の型は Void? になります。そのため、次のようなコードは正しく実行されます。
let a: Int? = 3
let result: Void? = a.map { print($0) } // Optional(())
Swift の Void は () (空のタプルの型)のシンタックスシュガーなので、 Void 型の値は () (空のタプル)になります。それが Optional に入っているので result は Optional(()) となります。
今回の処理では map の戻り値は必要ないので、 Optional(()) を返しているのはなんとなく不細工ですね。
Array には map の戻り値がないバージョンである forEach というメソッドがあります。
[2, 3, 5].forEach { print($0) } // 各要素を表示して改行
前節の通り Optional を要素数が最大 1 個のコレクションだと考えると、 Optional に forEach メソッドがあってもおかしくはありません。 forEach メソッドを Extension で実装しておけば 5 の解答は次のように書けます。
a.forEach { print($0) }
map だと値を変換する意味合いが強いですし、 Swift 2.0 からは map の戻り値を使わないと警告が出るようになってしまいました。このようなケースではコードの意図を明確化するために、また、警告を回避するために、僕は forEach を使うようにしています。
UnionとしてのOptional
CeylonのOptional
Ceylon (という言語)では Optional type の実装がユニークです。 Ceylon の Optional type は Union type を使って実現されています。
Union type 10とは、例えば、 Integer|String というように、 Integer か String のどちらかを表すというような型です。
Integer|String a = 123; // Integer も代入できる
Integer|String b = "abc"; // String も代入できる
当然、 Integer|String のままでは Integer や String として使うことはできません。
Integer|String a = 3;
Integer b = a * a; // コンパイルエラー → このままでは Integer として使えない
a を Integer として使うには次のようにします。
Integer|String a = 3;
if (is Integer a) { // このブロックの中では a は Integer として使える
Integer b = a * a; // 9
}
Ceylon の Integer? は Optional<Integer> ではなく Integer|Null のシンタックスシュガーです。 Null は唯一のインスタンス null を持つクラスで、何のメソッドも持ちません。そのため、 Integer|Null のままでは何のメソッドも呼び出すことができません11。
Integer? a = 3; // Integer|Null と等価
Integer? result;
if (is Integer a) { // このブロックの中では a は Integer として使える
result = a * a;
} else {
result = null;
}
Swift の Optional binding ( if let ... )そっくりですね!このように、 Optional<T> という型を導入しなくても、 Union type を使ってシンプルに Optional を実現することができます。
SwiftのOptional
実は、 Swift の Optional はこれに近い仕様になっています。 Swift では Optional は enum を使って次のように宣言されています(ただし、話の本題に関係ない部分は省略しています)。
enum Optional<T> {
case None
case Some(T)
}
enum は列挙された中のどれか一つを値として持つので、 Optional 型の変数は None ( nil に相当)か Some を値として持つことになります。また、 Swift の enum は Associated value と呼ばれる値を関連付けることができるため、 Some は T 型の値を持つことができます。これは( Some というタグを無視してしまえば)、 T|None という構造です。
Union type で Optional を実装する利点の一つとして、 Foo? に Foo 型の値をそのまま代入できることが挙げられます。 Foo|Bar 型の変数に Foo や Bar の値を代入できるのは当然なので、 Foo|Null 型の変数に Foo を代入できるのも当たり前です。
Integer? a = 3 // a は Integer|Null なので Integer を代入できるのは当たり前
しかし、 Optional<Foo> に Foo を代入できるのは不自然に思えます。実際、 Java ではこれはエラーになります。
Optional<Integer> a = 3; // コンパイルエラー
Optional<Integer> b = Optional.of(3); // Optional で包む必要あり
Swift ではなぜか Optional<Foo> に Foo をそのまま代入することができます。
let a: Optional<Int> = 3 // OK → Int|None っぽい挙動
しかし、自分で Optional のような enum を実装して同じことをするとコンパイルエラーになります。
enum MyOptional<T> { // 自作 Optional
case None
case Some(T)
}
let a: MyOptional<Int> = 3 // コンパイルエラー
let b: MyOptional<Int> = .Some(3) // Optional で包む必要あり
Java のときと同じですね!
どうやら、現時点では enum を Union type のように振る舞わせる機能は Optional や ImplicitUnwrappedOptional に限定されているようです12。
しかし、 Union type は Ceylon に限らず、 TypeScript への導入が計画されていたり、( Facebook がリリースした AltJS の) flow ではすでに採用されています。 Union type は Optional type と共に最近のプログラミング言語の潮流の一つだと思うので、将来的に Swift でも採用されるかもしれません。そして、 Optional との一貫性を考えると、 enum がその役割を果たすようになるのではないでしょうか。
Optional が Union type っぽく振る舞う性質のおかげで、「力試し」の 4 の解答は明示的に Optional に包む必要がないと考ることもできます。
// flatMap に渡す関数は Int? を return しなければならないので
// 本来はこのように Optional に包んで return しないといけないが、
a.flatMap { a0 in b.flatMap { b0 in Optional(a0 + b0) } }
// Int? を Int|None のように考えると
// Int|None を返さないといけないところで
// Int を返せるのは当たり前なのでので
// Optional で包まずに return できる
a.flatMap { a0 in b.flatMap { b0 in a0 + b0 } }
Union typeとサブタイピング
スーパータイプ、サブタイプと聞くとクラスの継承を思い浮かべる人が多いと思います。しかし、 Union type もスーパータイプ、サブタイプの関係を作ります。
Animal クラスを継承した Cat クラスがあるとき、 Animal は Cat のスーパータイプなので次のような代入が可能です。
let animal: Animal = Cat() // Animal は Cat のスーパータイプ
同様に、 Integer|String 型変数に Integer や String を代入できるというのは、 Integer|String が Integer や String のスーパータイプとして振舞うということです。
Integer|String a = 123; // Integer|String は Integer のスーパータイプ
Union type が本当にスーパータイプであるかを確かめるにはどうすれば良いでしょうか。
クラスを継承してメソッドをオーバーライドするとき、戻り値の型をサブタイプに狭めることができます。
class Foo {
func foo() -> Animal {
return Animal()
}
}
class Bar : Foo {
override func foo() -> Cat { // 戻り値の型を Cat に狭める
return Cat()
}
}
もし、 Integer が Integer|String のサブタイプであれば、同様のことができるはずです。次の Ceylon のコードは問題なく実行できます。
class Foo() {
shared default Integer|String foo() {
return 123;
}
}
class Bar() extends Foo() {
shared actual Integer foo() { // 戻り値の型を Integer に狭める
return 456;
}
}
同じことが Optional type にも言えるはずです。 Swift の Optional で次のコードを書くと正しく実行できます。
class Foo {
func foo() -> Optional<Int> {
return 123
}
}
class Bar : Foo {
override func foo() -> Int { // 戻り値の型を Int に狭める
return 456
}
}
これは Optional<Int> が Int のスーパータイプとして振舞っている証拠です。当然ですが、自作の MyOptional で同じことをやるとコンパイルエラーになります。ますます、 Swift の Optional は Union type っぽいですね!
「力試し」の回答者
僕が把握している限り、次のお二人が「力試し」にチャレンジして投稿して下さいました。 @Ushio@github さんの方法は flatMap と等価で、 @sora0077@github さんの方法は map と ?? を組み合わせた方法になっています。
- 力試し:SwiftのOptionalでこれできますか? にチャレンジ by @Ushio@github さん
- 力試し:SwiftのOptionalでこれできますか? に挑戦 by @sora0077@github さん
まとめ
- Optional は「10億ドルの誤り」を回避する素晴らしい仕様
- しかし、 Optional を使って書きづらいケースがある
-
Foo?ではなくOptional<Foo>だと考えればわかりやすい - Optional は「中身(値)が空かもしれない箱」と考えると良い
-
mapやflatMapを使えばそのようなケースにエレガントに対処できる - その背景には モナド という概念があり、
Optionalに限らない他の モナド も含めて統一的に扱える -
Optionalは要素数が 0 か 1 のコレクションとして考えることもできる -
Optionalは Union type の一種として考えることもできる
Optional の背景にあるこれらの概念や考え方を理解しておくことで、思考が整理され Optional を使うときの見通しがよくなります。 map や flatMap を使いこなして安全で楽しい Optional ライフを送りましょう!
-
便宜的に
nilをOptional型の値として説明していますが、厳密にはSwiftにnilという値は存在しません。 ↩ ↩2 -
Swift の クロージャ は他の言語で言う ラムダ式, 関数リテラル, 無名/匿名関数 などに当たります。必ずしも状態を保持しているわけではありません。 ↩
-
flatMapの他にbindなどと呼ばれたりもします。 Haskell では>>=という演算子で表します。 ↩ -
ですので、
flatMapを使わない場合の解答はx.map(safeSqrt) ?? nilとも書けます。 ↩ -
例えば、 $-1.0^{0.5}$ などは実数の範囲で解を持ちません。 ↩
-
"Java8でのプログラムの構造を変えるOptional、ただしモナドではない - きしだのはてな" より。なお、 Java 8 の
Optionalは最終仕様では モナド になりました。 ↩ -
Arrayを モナド として考えた場合、どのような箱としてイメージすればよいかは今度書きたいと思います。 ↩ -
「または」ではなく「かつ」を表す Intersection type (
Foo&Bar)もあって面白いです。関数の型を Intersection type で表すことでオーバーロードを表現することができたりします。 ↩ -
正確には、
IntegerとNullの共通のスーパークラスが何のメソッドも持たないからです。 ↩ -
また、 Swift にはジェネリックな型の Variance を提供する仕組みも欠けています。
ArrayやOptionalは Covariant ですが、自作の型を Covariant や Contravariant にすることはできません。 ↩