本投稿の個別の説明( 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 にすることはできません。 ↩