読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(6) - Closures、Enumerations

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Closuresクロージャ

序文として「クロージャとは何か?」がつらつらと書かれているんですが、知ってるなら読む必要はあんまりないです。

Swiftが「クロージャ」と言う下手に触れると即マサカリが飛んでくる言葉をどう定義しているのか、ぐらいはざっと知っておいてもいいと思いますが。

Closure Expressions

クロージャの例としてsortと言う関数を取り上げています。

sortの引数は

  1. Array<SomeClass>
  2. (SomeClass, SomeClass) -> Bool

となっているそうです。コード例ではSomeClassをStringにしています。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}
var reversed = sort(names, backwards)
// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

backwardsでtrueが返るとs1を、falseが返るとs2を先頭に持ってくるようにするみたいですね。

さりげなくStringに比較演算子が使われているんですが、これはアルファベット順で大小関係を決めているみたいです。

backwardsをクロージャで記述するとこんな感じになります。

reversed = sort(names, { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

うーん。何かイケてないですね。どうせ型推論が利くんだから型名いらなくね?ってパターンがこちらです。

reversed = sort(names, { s1, s2 in return s1 > s2 } )

まだイケてないですね。一つしか式を書いてないし、どうせBoolを返さなきゃいけないのはわかってるじゃん?returnって書かなくてもよくね?ってパターンがこれです。

reversed = sort(names, { s1, s2 in s1 > s2 } )

s1,s2じゃどうせなんのことかわからないし変数名もいらなくね?ってか、inって書くのだるくね?ってパターンもあります。

reversed = sort(names, { $0 > $1 } )

sortの第二引数のFunctionの引数は(SomeClass, SomeClass)なんでしょ?じゃあSomeClassに演算子オーバーロードが設定されてたらそれだけでいいんじゃないの?とか言うファンキーなパターンもあります。

// 中々キモいので使ってみたくなるけど、
// 冷静に考えるとそもそもこれが出来るケースはかなり限定的
reversed = sort(names, >)

Trailing Closures

「Trailing」は「末尾」って意味です。Functionを引数に受けるFunctionは、その中身を後ろにくっつけることが出来ます。

言葉にすると意味不明ですね。わかるなら「Rubyのブロックのアレがlambdaでも受け取ってくれる」と覚えてもいいですよ。

reversed = sort(names) { $0 > $1 }

Trailing Closuresの効果的な例としてArray#mapと言う関数が紹介されています。MapReduceのmapです。

Array<SomeClass>#mapが受け取る引数は「(SomeClass) -> AnotherClass」の一つです。mapの戻り値は「Array<AnotherClass>」です。…この説明必要ですかね?

let digitNames = [
    0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]


let strings = numbers.map {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

Capturing Values

こんな関数を返すfunctionがあったとします。

func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementor
}

この返ってきた関数(incrementor)を呼び出せば呼び出した分だけ、内部のrunningTotalの数は増えていきます。

let incrementByTen = makeIncrementor(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

incrementByTenとは別にmakeIncrementorを呼び出して新しいincrementorを取得します。

この時、incrementByTenと新しいincrementorではrunningTotalを共有しません。

let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40

Closures Are Reference Types

先ほどの例で既に4回呼び出しているincrementByTenを別の変数に渡します。

この場合はクロージャの参照を渡すことになるので、runningTotalの値は共有されます。

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

「both of those constants or variables will refer to the same closure」とのことなので、今この状態でincrementByTenを呼び出したら60が返ってくるんじゃないかなと思うんですが、ちょっと自信ないです。

Enumerations(列挙体)

Enumeration Syntax

Enumの宣言方法は以下の通りです。

enum CompassPoint {
    case North
    case South
    case East
    case West
}

CやObjective-Cとは違って「North = 0」「South = 1」と言うわけではありません。

カンマを使って複数同時に宣言することも出来ます。

enum Planet {
    case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

Enumのメンバは変数に格納することが出来ます。

var directionToHead = CompassPoint.West

この時点で「directionToHead」はCompassPoint.Westになっているわけですが、別のCompassPointのメンバを再代入したい場合はEnumの名前を省略することが出来ます。

directionToHead = .East

これは再代入のみのルールではなく、何らかのEnumのメンバが入るだろうと言う文脈ではすべて省略可能なようです。

Matching Enumeration Values with a Switch Statement

Enumでswitchを書くコード例。

directionToHead = .South
switch directionToHead {
case .North:
    println("Lots of planets have a north")
case .South:
    println("Watch out for penguins")
case .East:
    println("Where the sun rises")
case .West:
    println("Where the skies are blue")
}
// prints "Watch out for penguins"

勿論defaultを書くことも出来ます。

let somePlanet = Planet.Earth
switch somePlanet {
case .Earth:
    println("Mostly harmless")
default:
    println("Not a safe place for humans")
}
// prints "Mostly harmless"

Associated Values

Enumメンバ単位で関連する型(と言うか、タプル)を設定することが出来ます。

enum Barcode {
    case UPCA(Int, Int, Int)
    case QRCode(String)
}

Enumのメンバを何らかの変数に代入する際に、設定されているタプルの値を与えます。

var productBarcode = Barcode.UPCA(8, 85909_51226, 3)
productBarcode = .QRCode("ABCDEFGHIJKLMNOP")

これにより、switchでEnumを判断する際に、そのEnumのメンバに関連する値をバインドして取り出すことが出来るようになります。。

// 例ではletでバインドしているが、varでもOK

switch productBarcode {
case .UPCA(let numberSystem, let identifier, let check):
    println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case .QRCode(let productCode):
    println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."

全部let、もしくはvarでバインドするのであれば、「case [let|var] .Member]と言う省略形の構文を使うことが出来ます。

switch productBarcode {
case let .UPCA(numberSystem, identifier, check):
    println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case let .QRCode(productCode):
    println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."

要はこれ、パターンマッチをサポートしてるよ、ってことですよね。

Raw Values

Enumそのものに関連する型を設定することで、メンバに初期値を与えることが出来ます。この値のことをraw valueと呼んでいるようです。

enum ASCIIControlCharacter: Character {
    case Tab = "\t"
    case LineFeed = "\n"
    case CarriageReturn = "\r"
}

ちなみに、Intの場合は以下のようにするだけで自動で連番を振ってくれます。

enum Planet: Int {
    case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

toRawメソッドを呼ぶことでメンバに設定されているraw valueを取得することが出来ます。

let earthsOrder = Planet.Earth.toRaw()
// earthsOrder is 3

raw valueからEnumのメンバを取得する場合はfromRawメソッドです。設定されていない可能性もあるので、optionalが返って来ます。

let possiblePlanet = Planet.fromRaw(7)
// possiblePlanet is of type Planet? and equals Planet.Uranus

一応頑張って活用しようとした例がありますが、実際はこんなことしないと思います。(positionToFindが何らかのパラメータ、とかならありえるだろうけど。)

let positionToFind = 9

if let somePlanet = Planet.fromRaw(positionToFind) {
    switch somePlanet {
    case .Earth:
        println("Mostly harmless")
    default:
        println("Not a safe place for humans")
    }
} else {
    println("There isn't a planet at position \(positionToFind)")
}
// prints "There isn't a planet at position 9"