Match Me if you can: Swift Pattern Matching in Detail.が良記事で、ちょうど僕もSwift2のキャッチアップ中だったので、写経しながら翻訳ぽい記事を書きました。(翻訳許可もいただいています)
冒頭のイントロ的部分は飛ばして、Advanced Pattern Matchingから書きます。
様々なパターンマッチング
7つのパターンを紹介します。
switch
だけではなくif
・guard
・for
を組み合わせて表現します。
1. ワイルドカードパターン
ワイルドカードパターンは、マッチした値を無視します。
let _ = fn()
の_
と同様です。
単に_
で受けるとnil
を含む全ての値にマッチしてしまうので、オプショナルの値をマッチするには、_?
として区別します。
let p: String? = nil
switch p {
case _?: print("Has String")
case nil: print("No String")
}
enum
およびtuple
を受けることが出来ます。
switch (15, "example", 3.14) {
case (_, _, let pi): print("pi: \(pi)")
}
2. Identifierパターン
具体的な値をマッチさせます。
switch 5 {
case 5: print("5")
}
3. 値バインディングパターン
let
・var
で値を受けるものと同じで、その省略記法です。
switch (4, 5) {
case let(x, y): print("\(x) \(y)")
}
4. Tupleパターン
以下の例ではこのような処理になります。
- ageを展開
- jobが非nilである時にマッチさせるが値自体は不要
- payloadはNSDictionary型であるが、同様に値自体は不要
let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()
switch (age, job, payload) {
case (let age, _?, _ as NSDictionary):
print(age)
default:
break
}
5. 列挙型caseパターン
パターンマッチは、enum
の扱いに優れています。
下記のように、ステートが短い箇所ではstruct
の代用にもなります。
enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}
for e in [Entities.Player(x: 1, y: 2), Entities.Tank(x: 3, y: 4)] {
switch e {
case let .Soldier(x, y):
print("soldier, x: \(x), y: \(y)" )
case let .Tank(x, y):
print("tank, x: \(x), y: \(y)" )
case let .Player(x, y):
print("player, x: \(x), y: \(y)" )
}
}
6. タイプキャストパターン
is
では型判定のみでキャスト結果は無視されます。
as
で変数に割り当てることで、キャスト結果の参照が可能となります。
let a: Any = 5
switch a {
case is Int: print(a + 1) // コンパイルエラー(aはAnyのまま)
case let n as Int: print(n + 1) // nで受けつつキャストしているのでコンパイル通る
default: break
}
7. エクスプレッションパターン
値を~=
で受けてマッチします。
switch 5 {
case 0...10: print("In range 0-10")
default: break
}
(訳注: 以下のような比較方法を今回初めて知りました)
0...10 ~= 5 // true
~=
をオーバーライドすれば、下記のようなstructのマッチも可能となります。
struct Soldier {
let hp: Int
let x: Int
let y: Int
}
func ~= (pattern: Int, value: Soldier) -> Bool {
return pattern == value.hp
}
let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
case 0: print("dead soldier")
default: break
}
Custom pattern matching in Swift · Episteme and Techneなどにさらに詳しく書いてあります。
switch文のFallthrough・Break・Label
swiftのswitch
はデフォルトではfallthrough
しないので注意です。
逆に、break
を書かずともbreakしてくれます。
fallthrough
させたい場合は明示的に書く必要があります。
switch 5 {
case 5:
print("Is 5")
fallthrough
default:
print("Is a number")
}
// Will print: "Is 5" "Is a number"
このように早期returnぽくcase式を抜けたい時に、break
すると良いですね。
func getSystemUser(userId: Int) -> String? { return "hoge" }
func insertIntoRemoteDB(userData: String) { print(userData) }
let userType = "system"
let userID = 10
switch (userType, userID) {
case ("system", _):
guard let userData = getSystemUser(userID) else { break }
print("user info: \(userData)")
insertIntoRemoteDB(userData)
default: ()
}
また、このようにgameLoop
というlabelを付けておくと、switch文の中で、whileループを抜けられたり、その継続を出来たりします。
func state() -> State { return .GameOver }
func calculateNextState() { "calculateNextState" }
gameLoop: while true {
switch state() {
case .Waiting: continue gameLoop
case .Done: calculateNextState()
case .GameOver: break gameLoop
}
}
活用例
オプショナル
オプショナルをアンラップするのは色々な方法がありますが、そのうちの一つがパターンマッチを利用したものです。
func secretMethod() -> String? { return "secret" }
// Swift 2より前
var result: String? = secretMethod()
switch result {
case .None:
print("is nothing")
case let a:
print("\(a) is a value") // -> "Optional(secret is a value)"
}
// Swift 2から可能な書き方
switch result {
case nil:
print("is nothing")
case let a?:
print("\(a) is a value") // -> "secret is a value"
}
タイプマッチ
Swiftで型が強くなりましたが、Objective-CのAPIなど使っていると、こういう型が混ざったNSArrayなどを受け取ることがあります。
let u = NSArray(array: [NSString(string: "String1"), NSNumber(int: 20), NSNumber(int: 40)])
それをこのように型でマッチしつつハンドリングできます。
for x in u {
switch x {
case is NSString:
print("string")
case is NSNumber:
print("number")
default:
print("Unknown types")
}
}
グレード分けをrangeで
0から100点のスコアをrangeでのマッチを用いてグレードに分ける例です。
let aGrade = 84
switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default:
print("Incorrect Grade")
}
単語を使用数に応じてフィルター
使用数が3より多いものをフィルターする処理の例です。
map
とfilter
で処理出来ます。
let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]
let res = wordFreqs.filter({ (e) -> Bool in
if e.1 > 3 {
return true
} else {
return false
}
}).map { $0.0 }
print(res) // ["k", "a"]
さらに、flatmap
を使うと非nilの要素を返してくれるので、これだけで良いです。
let res = wordFreqs.flatMap { (e) -> String? in
switch e {
case let (s, t) where t > 3: return s
default: return nil
}
}
print(res)
フォルダ探索
パターンマッチを活用して、特定の名前・拡張子のファイルをフィルターして処理する例です。
guard let enumerator = NSFileManager.defaultManager().enumeratorAtPath("/customers/2014/")
else { return }
for url in enumerator {
switch (url.pathComponents, url.pathExtension) {
// psd files from customer1, customer2
case (let f, "psd")
where f.contains("customer1")
|| f.contains("customer2"): print(url)
// blend files from customer2
case (let f, "blend") where f.contains("customer2"): print(url)
// all jpg files
case (_, "jpg"): print(url)
default: ()
}
}
フィボナッチ数列
再帰処理をswitch-case・パターンマッチによって実現しています。
func fibonacci(i: Int) -> Int {
switch(i) {
case let n where n <= 0: return 0
case 1: return 1
case let n: return fibonacci(n - 1) + fibonacci(n - 2)
}
}
print(fibonacci(8))
レガシーAPIの値展開
こういう型の緩いレガシーAPIを扱わなくてはいけなくなった時の例です。
func legacyAPI(id: Int) -> [String: AnyObject] {
return ["type": "system", "department": "Dark Arts", "age": 57,
"name": ["voldemort", "Tom", "Marvolo", "Riddle"]]
}
このようにマッチする型の値を受け取りつつ、処理出来ます。
func createSystemUser(name: String, dep: String) {}
let item = legacyAPI(4)
switch (item["type"], item["department"], item["age"], item["name"]) {
case let (sys as String, dep as String, age as Int, name as [String]) where
age < 1980 &&
sys == "system":
createSystemUser(name.count == 2 ? name.last! : name.first!, dep: dep ?? "Corp")
default:()
}
if・for・guardを使ったパターン
Swiftのドキュメントは、全てのパターンは、if・for・guardで表現可能と言及しています。
これは、値バインディング・タプル・タイプキャストパターンをif・for・guardで記述した例です。
// This is just a collection of keywords that compiles. This code makes no sense
func valueTupleType(a: (Int, Any)) -> Bool {
// guard case Example
guard case let (x, _ as String) = a else { return false}
print(x)
// for case example
for case let (a, _ as String) in [a] {
print(a)
}
// if case example
if case let (x, _ as String) = a {
print("if", x)
}
// switch case example
switch a {
case let (a, _ as String):
print(a)
return true
default: return false
}
}
let u: Any = "a"
let b: Any = 5
print(valueTupleType((5, u)))
print(valueTupleType((5, b)))
// 5, 5, "if 5", 5, true, false
for case文
Swift 2で、switchの表現力が上がったために、パターンマッチングはより大事になりました。
例えば、オプショナル型の配列をフィルタして非オプショナル型の配列として返す関数はこう書けます。
func nonnil<T>(array: [T?]) -> [T] {
var result: [T] = []
for case let x? in array {
result.append(x)
}
return result
}
print(nonnil(["a", nil, "b", "c", nil]))
このように要素の値を受け取りつつさらにその値を判定して処理するコードが、こう簡潔に書けます。
enum Entity {
enum EntityType {
case Soldier
case Player
}
case Entry(type: EntityType, x: Int, y: Int, hp: Int)
}
for case let Entity.Entry(t, x, y, _) in [Entity.Entry(type: .Player, x: 1, y: 2, hp: 3)]
where x > 0 && y > 0 {
print("t: \(t), x: \(x), y: \(y)")
}
hpが0より多いSoldierがまだ残っていればゲームオーバーじゃないと判定するコードの例です。
func gameOver() -> Bool {
for case Entity.Entry(.Soldier, _, _, let hp) in [Entity.Entry(type: .Soldier, x: 1, y: 2, hp: 30)]
where hp > 0 {return false}
return true
}
print(gameOver()) // false
guard case文
まずはguard単体の簡単な使用例です。
func example(a: String?) {
guard let a = a else { return }
print(a) // このスコープでは、aが非オプショナルのString
}
example("yes")
guard caseでパターンマッチしつつ条件外の時はゼロを返して、それ以降のスコープではマッチした値を使って処理する例です。
let MAX_HP = 100
func healthHP(entity: Entity) -> Int {
guard case let Entity.Entry(.Player, _, _, hp) = entity
where hp < MAX_HP
else { return 0 }
return MAX_HP - hp
}
print("Soldier", healthHP(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79)))
print("Player", healthHP(Entity.Entry(type: .Player, x: 10, y: 10, hp: 57)))
// Prints:
"Soldier 0"
"Player 43"
if case文
guardと似てますが、その逆にif caseで囲ったスコープでマッチされた値を使って処理します。
func move(entity: Entity, xd: Int, yd: Int) -> Entity {
if case Entity.Entry(let t, let x, let y, let hp) = entity
where (x + xd) < 1000 &&
(y + yd) < 1000 {
return Entity.Entry(type: t, x: (x + xd), y: (y + yd), hp: hp)
}
return entity
}
print(move(Entity.Entry(type: .Soldier, x: 10, y: 10, hp: 79), xd: 30, yd: 500))
// prints: Entry(main.Entity.EntityType.Soldier, 40, 510, 79)
制限
エクスプレッションパターン(~=
で受けるやつ)はtupleに対して使えません。
また、Scala
のunapply
に対応するものもありません。
unapplyとは、init
の逆に、init
で受ける引数を返す定義をし、それをクラスのマッチに使える、というものです。
現状コンパイルエラーになりますが、擬似コードを書くと、こういうイメージです。
struct Imaginary {
let x: Int
let y: Int
func unapply() -> (Int, Int) {
// implementing this method would then in theory provide all the details needed to destructure the vars
return (self.x, self.y)
}
}
// this, then, would unapply automatically and then match
guard case let Imaginary(x, y) = anImaginaryObject else { break }
関連
最後に
Swift 2で特にパターンマッチ周りの言語仕様が増えて追い切れていませんでしたが、本記事書く過程で、かなり身に付きました。
特に、今一人iOSアプリ開発なので、コードレビューで人のコード見たり指摘されたりする機会が無いので、こういうキャッチアップ大事だなと改めて思いました。
良い感じに活用して、簡潔かつ意図の分かりやすいコードを書くように努めたいです( ´・‿・`)