この記事は 慶應義塾大学SFC村井&徳田研 Advent Calendar 2015 の4日目の記事です。
今朝、SwiftコンパイラのソースコードがGitHubで公開されました。
現時点で既にSwiftコンパイラはMac OS XだけでなくUbuntuの最新版でもコンパイルできることが公式にアナウンスされており、これからあらゆるプラットフォームでC++やJavaのようにSwiftを使えるようになっていくことは想像に難くありません。
以下では来るSwift全盛時代に備え、特に2.0以降のSwiftで特徴的なパターンマッチをどう読むべきか、どう読まれることを想定して使うべきか、についてまとめます。
ポイント
- パターンは使用箇所によって宣言的パターンと条件的パターンの2つに分けられる
- パターンマッチは続くブロックスコープで主役となる変数のリストを示す
この2つのポイントを全体を通して解説していきます。
パターンマッチとは
ここでいうパターンマッチとは、ある値をその型の持ちうる構造によって部分的あるいは全体的に具体化し、内包する値への分解をしたり、一致具合を条件式として利用する操作のことを指します。
このようなパターンマッチは従来から特に関数型言語で多く採用されており、たとえばHaskellでは関数の引数やガード構文でパターンマッチが常用されています。
power _ 0 = 1.0
power a n | n > 0 = a * power a (n - 1)
| otherwise = 1.0 / power a (- n)
data Tree a = Leaf a | Node a (Tree a) (Tree a)
sum (Leaf v) = v
sum (Node v l r) = v + (sum l) + (sum r)
これまでの多くの命令型言語では構造化された値はポインタや参照によって保持されているとする捉え方が主流であり、パターンマッチのようにそのまま値として使用する形式は実装されてきませんでした。
それに対しSwiftでは列挙型を構造的な値の構築に使用することでデータ型的な表現を受け入れ、パターンマッチを条件分岐やループ、エラー処理、変数束縛など様々なところで使えるようにしています。
indirect enum Tree {
case Leaf(Int)
case Node(Int, Tree, Tree)
}
func sum(tree: Tree) -> Int {
switch tree {
case let .Leaf(v):
return v
case let .Node(v, l, r):
return v + sum(l) + sum(r)
}
}
しかし、Swiftのパターンマッチでは書かれる場所によって使えるパターンが決まっており、その分かりづらさが時にプログラマを混乱させています。
各場所で使えるパターン・使えないパターン
まず、Swiftでは以下9つのパターンが用意されています。
while case _ = a {} // wildcard-pattern
let x = a // identifier-pattern
if case let x = a {} // value-binding-pattern
for (x, y) in xs {} // tuple-pattern
if case .Leaf = t {} // enum-case-pattern
if case 0? = x {} // optional-pattern
if case is Int = a {} // type-pattern
if case _ as Int = a {} // type-casting-pattern
if case 0 = a {} // expression-pattern
しかし、次に示しているように、これらのパターンは場所によって必ずしも使えるわけではありません。
for .Leaf in ts {} // for-in文ではenum-case-patternを使用できない
if case x = a {} // if-case文ではidentifier-patternを使用できない
if let x as Int = y {} // optional-bindingではtype-casting-patternを使用できない
宣言的パターンと条件的パターン
このパターンの性質の違いはAppleのドキュメントでは明確に定義されていませんが、各場所で使えるパターンと使えないパターンを見ていると、これらを二通りに分けることができることに気づきます。
宣言的パターン
宣言的パターンはcaseの付かないfor-in文やlet、var束縛、optional-bindingといった値の束縛が前提となる箇所で使用できるパターン群です。
宣言的パターンのタプル内で使用出来るパターンも宣言的パターンに限られます。
let _ = a // wildcard-pattern
let x = a // identifier-pattern
for (_, v) in xs {} // tuple-pattern (wildcard-pattern, identifier-patternと組み合わせて使用)
すでに値の束縛が発生することが前提のため、value-binding-patternはletやvarのネストとなり使用できませんし、値を束縛しないexpression-patternやtype-patternも使用できません。
条件的パターン
条件的パターンはif、while、switchのcaseなど、パターンにマッチするかどうかによって条件分岐が発生する箇所で使用できるパターン群です。
宣言的パターンに挙げた以外のパターンマッチではこちらのパターン群が使用されます。
while case _ = a {} // wildcard-pattern
if case let x? = a {} // value-binding-pattern (optional-patternと組み合わせて使用)
if case (0, let v) = t {} // tuple-pattern (value-binding-patternとexpression-patternを組み合わせて使用)
if case .Leaf = t {} // enum-case-pattern
if case 0? = x {} // optional-pattern (expression-patternと組み合わせて使用)
if case is Int = a {} // type-pattern
if case let x as Int = a {} // type-casting-pattern (value-binding-patternと組み合わせて使用)
if case 0 = a {} // expression-pattern
宣言的パターンと比べて多くのパターンを利用できますが、value-binding-patternが使用されない限り変数は値への参照としての役割になりますので、IdentifierPatternは使用できません。
重複するように見える構文の意義
上記二つの分類を見て分かる通り、宣言的パターンと条件的パターンではそもそもその使われ方が異なります。
Swift 2.0が出た当初にoptional-bindingがあるのに、optional-patternはどういったところで使うのかわからない、といった議論がありましたが、optional-bindingでは宣言的パターンしか使えないことを考えると、他のより複雑なパターン(例えばtype-casting-pattern)と組み合わせて使えるようにするためには必要だったことがわかります。
if case (let x as Int)? = a {} // optional-pattern, value-binding-pattern, type-casting-patternを組み合わせて使用
if let x as Int = a {} // optional-binding-patternではtype-casting-patternを使用できず、エラーとなる
ブロックの登場人物のリストを与える条件的パターン
また、条件的パターンは続くブロックの実行を行うかどうかを決定する役割も担うことから、自然とそのブロック内で重要な働きをする変数を明示することにもなります。
if case .Fatal(msg) = error { // このブロック内ではmsgが使用されることが想像できる
...
print(msg)
...
}
まとめ
Swiftのパターンマッチでは各パターンの意味によってその役割が明確に分担されており、明確な使用箇所の判断ができることを見てきました。
また、条件的パターンはブロックの制御を担うことから、コードの読み手にそのあとに続くコードの意味を特徴付ける情報を提示してくれることも確認しました。
これからまだ新しいパターンが増えてくる可能性は大いにありますが、どんな場面でどういった意味を担うのかをそのコンテキストから明らかにできることに気づいていれば、きっとそれらを使いこなしていくことができるのではないでしょうか。