320
327

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift 2のパターンマッチ構文集(ほぼ翻訳)

Last updated at Posted at 2015-09-13

Match Me if you can: Swift Pattern Matching in Detail.が良記事で、ちょうど僕もSwift2のキャッチアップ中だったので、写経しながら翻訳ぽい記事を書きました。(翻訳許可もいただいています)

冒頭のイントロ的部分は飛ばして、Advanced Pattern Matchingから書きます。

様々なパターンマッチング

7つのパターンを紹介します。
switchだけではなくifguardforを組み合わせて表現します。

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. 値バインディングパターン

letvarで値を受けるものと同じで、その省略記法です。

switch (4, 5) {
case let(x, y): print("\(x) \(y)")
}

4. Tupleパターン

以下の例ではこのような処理になります。

  1. ageを展開
  2. jobが非nilである時にマッチさせるが値自体は不要
  3. 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より多いものをフィルターする処理の例です。
mapfilterで処理出来ます。

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に対して使えません。

また、Scalaunapplyに対応するものもありません。
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アプリ開発なので、コードレビューで人のコード見たり指摘されたりする機会が無いので、こういうキャッチアップ大事だなと改めて思いました。
良い感じに活用して、簡潔かつ意図の分かりやすいコードを書くように努めたいです( ´・‿・`)

320
327
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
320
327

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?