SwiftのOptional型を極める

  • 936
    いいね
  • 8
    コメント

本投稿の個別の説明( Optional とは何か、 ?.map, flatMap の関係、その背後にあるモナドという概念)は 2017 年現在でも通用するものですが、 Swift の Optional の使い方としては、僕の考えとのズレが大きくなってきました。 Swift の Optional をいつ・どのように使うべきかについてもまとめた ので、そちらも併せて御覧下さい。


Optional は Swift の仕様の中でもっとも素晴らしいものの一つだと、僕は考えています。

null参照 (多くの言語で nilnull と呼ばれるもの)を発明したトニー・ホーアは次のように述べています1

それは10億ドルにも相当する私の誤りだ。null参照を発明したのは1965年のことだった。(中略)これは後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。

その「10億ドルの誤り」からプログラマを開放してくれるのが Optional です。

しかし、実際に Optional を使い始めると、コードを書きづらいパターンがいくつかあることに気付きます。本投稿では、 Optional の背景にある概念について説明し、そのようなケースのエレガントな解決法を示します。

本投稿の主なキーワード

  • map, flatMap
  • 第一級関数, 高階関数, 高階メソッド
  • モナド
  • Union type

Optionalで書きづらいケース

次のようなケース(先日「力試し」として投稿したものです)では、 Optional bindingif let ... )などで書こうとするとコードが複雑になってしまいます。

Optional を使って次のことをやる場合に、 簡潔かつ安全 に書く方法を考えて下さい。

  1. a: Int? があるとき、 a の値を二乗したい。ただし、 anil の場合は nil を得たい。
  2. array: [Int]? があるとき、 array の最初の要素を取得したい。ただし、 arraynil か、最初の要素が存在しない( Array が空の)場合は nil を得たい。(ヒント: [Int]first: Int? というプロパティを持つ。 first は最初の要素が存在しない場合は nil を返す。)
  3. x: Double? があるとき、 x の平方根を計算したい。ただし、 xnil または負の数の場合は nil を得たい。なお、 sqrt を安全にした(負の数を渡すと nil を返す)関数 safeSqrt: Double -> Double? があるものとして考えて良い。
  4. a: Int?b: Int? があるとき、 a + b を計算したい。ただし、 abnil の場合には nil を得たい。
  5. a: Int? があるとき、 a の値を print で表示したい。ただし、 anil の場合には何も表示したくない。

1, 2, 4 では結果の型が( Int ではなく) Int? に、 3 では Double? になることに注意して下さい。また、 3 の safeSqrt は次のように実装できます。

func safeSqrt(x: Double) -> Double? {
    return x < 0.0 ? nil : sqrt(x)
}

例えば、 1 の答えを Optional binding を使って書くと次のようになります。

let result: Int?
if let a0 = a {
    result = a0 * a0
} else {
    result = nil
}

ただ値を二乗したいだけなのに複雑すぎます。もし aInt? ではなく Int なら
a * a で済むコードが 6 行にもなってしまっています。

別の方法として、 nil チェックと三項演算子、 Forced unwrapping! )を組み合わせれば 1 行で次のように書くことはできます。

let result: Int? = a == nil ? nil : a! * a!

しかし、 ! は安全ではないので極力使いたくないですし、短いだけで簡潔とも言いづらいです。そもそも、 nil チェックが必要なんて null参照 時代に逆戻りでせっかくの Optional が台無しです。

Optionalの考え方

解答の前に、まずはどのようなイメージで Optional をとらえればよいか説明します。

Optionalnil も入れられる型 だと考えている人は多いと思います。よくあるのは次のような理解でしょうか。

  • Foo 型の変数には nil を代入できないけど、 Foo? にすれば nil も入れられる。
  • Foo? 型の変数はそのままでは使えないので Optional bindingif let = ... )や Forced unwrapping! )を使って Foo 型に変換してから使う。

これらは間違ってはいないですが、 Optional の正確な理解とは言えません。

Foo? は Optional<Foo> と等価

まず、 Swift の OptionalOptional という一つの型です。 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 とはまったく異なる型だからです。 StringInt として使えないのと同じ話です

なお、 Optional<Foo>Foo のメソッドを呼ぶことはできませんが Optional のメソッドを呼ぶことはできます。 nilOptional 型の値なので2 nil に対して Optional のメソッドを呼び出すことも可能です。

nilに対してメソッドを呼び出す
let a: Optional<Int> = nil
a.getMirror() // Optional<Int> の持つ getMirror メソッドを呼ぶ → エラーにならない

Foo?nil も入れられる型だと考えていたら、なんで nil に対してメソッドを呼ぶことができるのか理解しづらいと思います。 Swift の nilOptional 型の値2であって null参照 とはまったく別の概念です。

Optional について混乱したときには、 Foo? ではなく Optional<Foo> だと考えてみることをオススメします。ただそれだけのことで考えを整理しやすくなると思います。

Optionalは箱と考える

Foo?nil も入れられる型だと考えないのであれば、どのようなイメージでとらえると良いでしょうか。僕は、 Foo?Foo 型の値を入れられる箱だと考える とわかりやすいと思います。

箱の中身は空( nil )かもしれないし、値が入っているかもしれません。値を使うには箱から取り出さなければ( unwrap しなければ)いけません。

このイメージの違いは些細なことに感じられるかもしれませんが、箱というより正確なメタファーで与えることで、次のような複雑なケースもシンプルに理解することができます。

let a: Int?? = 3

Optionalnil も入れられる型だと考えていると Int?? を理解しづらいです。しかし、 Optional を箱だと考えると、 Int??Optional<Optional<Int>> )は箱が二重になっており、内側の箱の中に Int が入ってるとイメージできます。箱が二重になっているなら、 箱を二回開けて中身を取り出さないと中の値を使えないことがすぐにわかります。

箱を二回開けて中身を取り出す
let a: Int?? = 3
if let a0: Int? = a { // 外の箱を開けて内の箱を取り出す
    if let a1: Int = a0 { // 内の箱を開けて値を取り出す
        println(a1) // 3
    }
}

箱から取り出さずに中身を操作する

Optionalnull参照 と違って安全で素晴らしいですが、 Optional を使った安全なコードを書いているとすぐに箱( Optional )だらけになってしまいます。そして、「力試し」の 1 (下に再掲)のように箱の中身をちょっと操作したいと思っただけで、わざわざ箱から値を取り出して操作してもう一度箱に詰めるというわずらわしい作業が必要になります。

1. a: Int? があるとき、 a の値を二乗したい。ただし、 anil の場合は nil を得たい。

できることなら、箱から中身を取り出さずに箱の中身だけ操作できれば簡単です。 Optional には、そのための道具 map メソッドが用意されています。

1の解答
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 メソッドの型は次のように解釈されます。

aにsquareを渡した場合のmapメソッドの型
func map(f: Int -> Int) -> Int?

Optionalmap メソッドは次のような挙動をします。

  • もし箱の中身が空( nil )だったら空の箱( nil )を返す。
  • 箱に値が入っていたら、その値を渡された関数 f に適用する。そして、適用した結果を新しい箱に詰めて返す。

先程の square の例では、 a の箱には 3 が入っているので、 square3 が渡され 9 が得られます。そして、それが Optional に包まれて return されます。

クロージャ

関数 squaremap メソッドを使えば、箱から値を取り出さずに中身を二乗することができました。しかし、箱の中を操作する度にわざわざ関数を作るのは面倒です。 Swift には関数をその場で記述する構文 クロージャ があります3

例えば、 func を使って square を定義する代わりに、 クロージャ を使って次のように書くことができます。

let square: Int -> Int = { (x: Int) -> Int in
    return x * x
}

クロージャ のブロック内に式が一つだけ書かれている場合には、その式の結果が自動的に戻り値として使われます。そのため、 return を省略して次のように書けます。

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 では、関数やメソッドの引数として クロージャ を渡す場合、最後の引数については クロージャ を外出しして iffor のブロックのように書くことができます。

let result: Int? = a.map() { $0 * $0 }

また、その結果 () の中に書く引数がなくなった場合には、 () も省略することができます。

()を省略
let result: Int? = a.map { $0 * $0 }

これで、本節冒頭と同じシンプルなコードが得られました。

Optionalの考え方 (2)

次の問題に進む前に、 Optional の別の側面について説明します。

Optionalを使って失敗を表す

StringtoInt メソッドは StringInt に変換するメソッドです。しかし、 "abc" のような文字列は Int としてパースすることができないので失敗します。失敗時には Int の代わりに nil が返されます。そのため、 toInt の戻り値の型は Int ではなく Int? になります。このことは大したことに思えないかもしれませんがとても重要です。

従来の null参照 の言語でも、関数やメソッドが処理に失敗したときに nilnull を返すことはよくあります。 Objective-C のライブラリはそのように設計されていますし、例外機構を持つ Java なんかでも失敗時に(例外ではなく) null が返ってくることも多いです。

Swift で nil が返された場合にそれらの言語と決定的に異なっているのは、 Optional は値を使う前に必ず nil かどうかチェックしなければならない ことです。

let numberOrNil: Int? = string.toInt()
if let number = numberOrNil { // そのままでは使えないので分岐
    // パースに成功したときの処理
} else {
    // パースに失敗したときの処理
}

Objective-C や Java では nilnull を返すメソッドに対して、失敗した場合の処理を無視しても問題なくコンパイルできます。そのため、意図しないエラー処理忘れが頻発し、多くのバグを生み出してきました。 null参照 を排除して Optional を導入することで、プログラマにエラー処理を強制し、そのようなバグを未然に防ぐことができるのです

  • Optional は単に nil を入れられるというだけでなく 失敗かもしれない値を表す ことがある。
  • 失敗かもしれない値を Optional で表すことで エラー処理を強制することができる

上記の二点を意識して、 Optional を見たときには失敗とエラー処理を意図しているのかもしれないという視点で見てみましょう。

箱の中身を操作する(失敗するかもしれない場合)

2. array: [Int]? があるとき、 array の最初の要素を取得したい。ただし、 arraynil か、最初の要素が存在しない( 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 の答えは簡単に書けます。

2の解答
let result: Int? = array?.first

しかし、 3 のようなケースでは Optional chaining でうまく書くことができません。

3. x: Double? があるとき、 x の平方根を計算したい。ただし、 xnil または負の数の場合は nil を得たい。なお、 sqrt を安全にした(負の数を渡すと nil を返す)関数 safeSqrt: Double -> Double? があるものとして考えて良い。

2 と 3 は、箱の中身に対して失敗するかもしれない処理をしたいという意味では同じです。しかし、 Optional chaining は、箱の中身に対してメソッドをコールすることはできても、箱の中身を引数に渡すことはできません。

とりあえず、箱が二重になってしまいますが map で書いてみると次のようになります。

let result: Int?? = x.map { safeSqrt($0) } // 箱が二重になる以外はうまく動く

今、二重の箱が邪魔なので外側の箱だけ開けたいです。でも ! で開けるわけにはいきません。もし外側の箱が空( nil )だったらクラッシュしてしまうからです。

そこで ?? を使います。 ??! 同様に箱を空けますが、箱が空だった場合には代わりの値で代用します。 a ?? b と書くと、 anil でなければ a の箱の中身を、 nil だった場合には b を返します。

??の使い方
// 結果の型が Int? ではなく Int なことに注意
let result1: Int = "2".toInt() ?? -1 // 2
let result2: Int = "X".toInt() ?? -1 // -1

?? は箱を一つ開けるので、 ?? を使えば二重の箱を一重にすることができます。

3の解答(1)
// 結果の型が Int?? ではなく Int? なことに注意
let result: Int? = x.map { safeSqrt($0) } ?? nil

flatMap

このような処理( map の結果の二重の箱を一重にしたい )はよく行われるので flatMap というメソッドが用意されています4

flatMap を使えば 3 の解答は次のように書けます。

3の解答(2)
a.flatMap { safeSqrt($0) }

よりシンプルになりました。しかし、実はもっとシンプルに書くこともできます。

3の解答(3)
a.flatMap(safeSqrt)

map { ... }flatMap { ... } の書き方に慣れてくるとどんな場合でもクロージャで { ... } と書いてしまいがちですが、箱の中身に適用したい関数がすでに存在しているなら、単にその関数を渡せば良いのです5。高階関数の説明で a.map(square) と書いたのと同じです。

flatMapの役割

OptionalflatMap は失敗するかもしれない連続した処理を書く場合に役に立ちます。

Optional chaining は同じ目的で使われますが、 3 の解答で見たように flatMapOptional 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 chainingflatMap の一部のケースを簡単に書くためのシンタックスシュガー だと考えておくとわかりやすいです。

複数の箱の中身を使う

4. a: Int?b: Int? があるとき、 a + b を計算したい。ただし、 abnil の場合には nil を得たい。

この問題を解くには、二つの箱の中身を使って計算をする必要があります。箱を空けずに中身を使いたいので map で書いてみましょう。

let result: Int?? = a.map { a0 in b.map { b0 in a0 + b0 } }

二つの箱の中身を取り出すために map を入れ子にして使っています。この場合は 2 や 3 とは違って処理(足し算)は失敗はしませんが、 map を二重の入れ子にしたために結果も二重の箱( Int?? )になってしまっています。

flatMap

二重の箱をフラット(一重)にするために、ここでも flatMap が役に立ちます。

4の解答(1)
let result: Int? = a.flatMap { a0 in b.map { b0 in a0 + b0 } }

今、入れ子の内側が flatMap ではなく map になっていますが、これは a0 + b0 が失敗しない処理だからです。失敗する処理をしたい場合には内側の mapflatMap でなければなりません。

例えば、 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 の入れ子はやや複雑に思えるかもしれません。 アプリカティブスタイル を使うとよりシンプルに書くことができます。

4の解答(2)
let result: Int? = (+) <%> a <*> b

アプリカティブスタイル については本題から外れてしまうのでここでは説明しません。アプリカティブスタイルの使い方については "まだSwiftyJSONで消耗してるの?#アプリカティブスタイル" に、実装の仕方については "Swiftでアプリカティブスタイル" により詳しい説明があります。 Swift でアプリカティブスタイルを使うには、 thoughtbot/Runes を使うのが一般的です。

モナドとしてのOptional

Swift の Optionalモナド です。

モナド とは何でしょうか。 モナド の定義7を読んでも圏論を知らないと理解するのは難しいです。プログラミングにおけるモナドについてはこちらで詳しく説明していますので御覧下さい。ここではその中から僕が一番しっくりきた説明8を簡単に紹介します。

モナドというのは、モナドでくるまれた中の世界ではモナドを気にせずに処理が記述でき、外からみるとモナドでくるまれているという、外と中を分離するための仕組みです。

Optional bindingif let ... )や Forced Unwrapping! )を使って Optional の中身を取り出すのは、分離されている箱の中の世界と外の世界をつなげてしまう行為です。 mapflatMap を使うことで、中と外を分離したまま中の世界に対する処理を記述することができる のです。

Promise と比べてみる

Optional だけを見て モナド をイメージするのは難しいので、他の モナド と比較してみましょう。ここでは Promise という モナド を取り上げます。

Promise は JavaScript でよく使われ、次期 JavaScript にも正式採用されたので知っている人も多いかもしれません。 Promise は主に非同期処理で使われ、今はまだないけど将来的に得られる値を表します。

例えば、サーバと通信して値を取得したいとします。しかし、サーバからのレスポンスを待っていると、その間 UI が固まってしまうなどの問題があります。通常はコールバックで結果を受け取るのですが、それだとその値はコールバックの中でしか使えず、取り回すことができないので不便です。また、連続した非同期処理を書くときには、コールバックのネストがどんどん深くなってしまいます。

Optional「中身(値)が空かもしれない箱」 なら、 Promise は、 「今はまだないけど将来的に中身(値)が得られる箱」 です。サーバ通信などの非同期処理を行う関数が、コールバックで結果を返す代わりに Promise という箱を返すことで、サーバレスポンスを待たずに結果を取り回すことができるようになります。

例を見てみましょう(ここでは、 Promisethenmap および 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 のコードを比べてみましょう。

Promise
// 非同期的に得られた結果を二乗
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) } }
Optional
// 失敗するかもしれない処理の結果を二乗
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) } }

二つのコードは PromiseOptional が違うだけでそっくりですね! モナド という共通の性質を備えているから PromiseOptional は同じ方法で扱うことができる のです。

mapflatMap を使った書き方は一見難しそうに見えるかもしれません。しかし、一度慣れてしまえば、 Optional に限らず他の モナド も統一的な方法で扱うことができるようになり、便利でわかりやすいものです。是非 mapflatMap を習得して下さい。

その他のモナドの例としては、 Either について "Swift 3.0で追加されそうなEitherについて" を書いたので御覧下さい。

ArrayとOptional

ここでは、 Optional の別の顔について説明します。

実は Array を使えば Optional を表すことができます。 Optional は中身が空かもしれない箱でした。別の見方をすれば、 Optional を最大で一つしか要素を入れられない Array と考えることができます。

Optional を使った次のようなコードを考えます。

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モナド なので9mapflatMap を使うことができます。

Array
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 の値を print で表示したい。ただし、 anil の場合には何も表示したくない。

この問題は map を使えば簡単に解けます。

5の解答(1)
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 に入っているので resultOptional(()) となります。

今回の処理では map の戻り値は必要ないので、 Optional(()) を返しているのはなんとなく不細工ですね。

Array には map の戻り値がないバージョンである forEach というメソッドがあります。

[2, 3, 5].forEach { print($0) } // 各要素を表示して改行

前節の通り Optional を要素数が最大 1 個のコレクションだと考えると、 OptionalforEach メソッドがあってもおかしくはありません。 forEach メソッドを Extension で実装しておけば 5 の解答は次のように書けます。

5の解答(2)
a.forEach { print($0) }

map だと値を変換する意味合いが強いですし、 Swift 2.0 からは map の戻り値を使わないと警告が出るようになってしまいました。このようなケースではコードの意図を明確化するために、また、警告を回避するために、僕は forEach を使うようにしています。

UnionとしてのOptional

CeylonのOptional

Ceylon (という言語)では Optional type の実装がユニークです。 Ceylon の Optional typeUnion type を使って実現されています。

Union type 10とは、例えば、 Integer|String というように、 IntegerString のどちらかを表すというような型です。

Ceylon
Integer|String a = 123; // Integer も代入できる
Integer|String b = "abc"; // String も代入できる

当然、 Integer|String のままでは IntegerString として使うことはできません。

Ceylon
Integer|String a = 3;
Integer b = a * a; // コンパイルエラー → このままでは Integer として使えない

aInteger として使うには次のようにします。

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

Ceylon
Integer? a = 3; // Integer|Null と等価
Integer? result;
if (is Integer a) { // このブロックの中では a は Integer として使える
    result = a * a;
} else {
    result = null;
}

Swift の Optional bindingif let ... )そっくりですね!このように、 Optional<T> という型を導入しなくても、 Union type を使ってシンプルに Optional を実現することができます。

SwiftのOptional

実は、 Swift の Optional はこれに近い仕様になっています。 Swift では Optionalenum を使って次のように宣言されています(ただし、話の本題に関係ない部分は省略しています)。

Swift
enum Optional<T> {
    case None
    case Some(T)
}

enum は列挙された中のどれか一つを値として持つので、 Optional 型の変数は Nonenil に相当)か Some を値として持つことになります。また、 Swift の enumAssociated value と呼ばれる値を関連付けることができるため、 SomeT 型の値を持つことができます。これは( Some というタグを無視してしまえば)、 T|None という構造です。

Union typeOptional を実装する利点の一つとして、 Foo?Foo 型の値をそのまま代入できることが挙げられます。 Foo|Bar 型の変数に FooBar の値を代入できるのは当然なので、 Foo|Null 型の変数に Foo を代入できるのも当たり前です。

Ceylon
Integer? a = 3 // a は Integer|Null なので Integer を代入できるのは当たり前

しかし、 Optional<Foo>Foo を代入できるのは不自然に思えます。実際、 Java ではこれはエラーになります。

Java
Optional<Integer> a = 3; // コンパイルエラー
Optional<Integer> b = Optional.of(3); // Optional で包む必要あり

Swift ではなぜか Optional<Foo>Foo をそのまま代入することができます。

Swift
let a: Optional<Int> = 3 // OK → Int|None っぽい挙動

しかし、自分で Optional のような enum を実装して同じことをするとコンパイルエラーになります。

Swift
enum MyOptional<T> { // 自作 Optional
    case None
    case Some(T)
}

let a: MyOptional<Int> = 3 // コンパイルエラー
let b: MyOptional<Int> = .Some(3) // Optional で包む必要あり

Java のときと同じですね!

どうやら、現時点では enumUnion type のように振る舞わせる機能は OptionalImplicitUnwrappedOptional に限定されているようです12

しかし、 Union type は Ceylon に限らず、 TypeScript への導入が計画されていたり、( Facebook がリリースした AltJS の) flow ではすでに採用されていますUnion typeOptional type と共に最近のプログラミング言語の潮流の一つだと思うので、将来的に Swift でも採用されるかもしれません。そして、 Optional との一貫性を考えると、 enum がその役割を果たすようになるのではないでしょうか。

OptionalUnion 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 クラスがあるとき、 AnimalCat のスーパータイプなので次のような代入が可能です。

Swift
let animal: Animal = Cat() // Animal は Cat のスーパータイプ

同様に、 Integer|String 型変数に IntegerString を代入できるというのは、 Integer|StringIntegerString のスーパータイプとして振舞うということです。

Ceylon
Integer|String a = 123; // Integer|String は Integer のスーパータイプ

Union type が本当にスーパータイプであるかを確かめるにはどうすれば良いでしょうか。

クラスを継承してメソッドをオーバーライドするとき、戻り値の型をサブタイプに狭めることができます。

Swift
class Foo {
    func foo() -> Animal {
        return Animal()
    }
}

class Bar : Foo {
    override func foo() -> Cat { // 戻り値の型を Cat に狭める
        return Cat()
    }
}

もし、 IntegerInteger|String のサブタイプであれば、同様のことができるはずです。次の Ceylon のコードは問題なく実行できます。

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 の OptionalUnion type っぽいですね!

「力試し」の回答者

僕が把握している限り、次のお二人が「力試し」にチャレンジして投稿して下さいました。 @Ushio@github さんの方法は flatMap と等価で、 @sora0077@github さんの方法は map?? を組み合わせた方法になっています。

まとめ

  • Optional は「10億ドルの誤り」を回避する素晴らしい仕様
  • しかし、 Optional を使って書きづらいケースがある
  • Foo? ではなく Optional<Foo> だと考えればわかりやすい
  • Optional は「中身(値)が空かもしれない箱」と考えると良い
  • mapflatMap を使えばそのようなケースにエレガントに対処できる
  • その背景には モナド という概念があり、 Optional に限らない他の モナド も含めて統一的に扱える
  • Optional は要素数が 0 か 1 のコレクションとして考えることもできる
  • OptionalUnion type の一種として考えることもできる

Optional の背景にあるこれらの概念や考え方を理解しておくことで、思考が整理され Optional を使うときの見通しがよくなります。 mapflatMap を使いこなして安全で楽しい Optional ライフを送りましょう!



  1. "アントニー・ホーア - Wikipedia" より。 

  2. 便宜的に nilOptional 型の値として説明していますが、厳密にはSwiftにnilという値は存在しません。 

  3. Swift の クロージャ は他の言語で言う ラムダ式, 関数リテラル, 無名/匿名関数 などに当たります。必ずしも状態を保持しているわけではありません。 

  4. flatMap の他に bind などと呼ばれたりもします。 Haskell では >>= という演算子で表します。 

  5. ですので、 flatMap を使わない場合の解答は x.map(safeSqrt) ?? nil とも書けます。 

  6. 例えば、 $-1.0^{0.5}$ などは実数の範囲で解を持ちません。 

  7. モナド の定義はこちらにあります。 

  8. "Java8でのプログラムの構造を変えるOptional、ただしモナドではない - きしだのはてな" より。なお、 Java 8 の Optional最終仕様では モナド になりました。 

  9. Arrayモナド として考えた場合、どのような箱としてイメージすればよいかは今度書きたいと思います。 

  10. 「または」ではなく「かつ」を表す Intersection typeFoo&Bar )もあって面白いです。関数の型を Intersection type で表すことでオーバーロードを表現することができたりします。 

  11. 正確には、 IntegerNull の共通のスーパークラスが何のメソッドも持たないからです。 

  12. また、 Swift にはジェネリックな型の Variance を提供する仕組みも欠けています。 ArrayOptionalCovariant ですが、自作の型を CovariantContravariant にすることはできません。