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

【Swift】Appleの新言語「Swift」のリファレンスを読む(5) - Functions

注意

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

Functions(関数)

さてさてようやく楽しいところに入り始めました。

選ばれし人間はSwiftの関数定義を見るとCPUの歓声が聞こえてくると専らの噂ですが、私は単なる自己関手の圏におけるモノイド対象だけで記述していく言語の使い手ではないですし、どんなに耳を済ませてもファンの音でかき消されてしまいます。

ま、そんなことはどうでもいいです。関数型プログラミング言語のパラダイムに少しでも触れていないと理解し辛い概念がそこそこ出てきますが、なぁに、javascriptでもかじってれば十分でしょう。

Defining and Calling Functions

Swiftでの関数定義の基本形はこんな感じです。

func sayHello(personName: String) -> String {
    let greeting = "Hello, " + personName + "!"
    return greeting
}

sayHelloが関数名、personNameが引数名、Stringが引数の型、-> Stringが戻り値の型です。

呼び出し方もシンプルです。

println(sayHello("Anna"))
// prints "Hello, Anna!"
println(sayHello("Brian"))
// prints "Hello, Brian!"

「はじめてのプログラミング」みたいな記事でもないのでこの辺はささっと飛ばしましょう。

Function Parameters and Return Values

複数の引数だとか、引数のない関数だとか、戻り値のない関数の定義方法が書いてあります。大体あなたの想像通りでしょう。

func halfOpenRangeLength(start: Int, end: Int) -> Int {
    return end - start
}
println(halfOpenRangeLength(1, 10))
// prints "9"


func sayHelloWorld() -> String {
    return "hello, world"
}
println(sayHelloWorld())
// prints "hello, world"

func sayGoodbye(personName: String) {
    println("Goodbye, \(personName)!")
}
sayGoodbye("Dave")
// prints "Goodbye, Dave!"

ちなみに、最後のsayGoodbyeと言う関数は厳密に言うとVoid型の値を返しています。中身は要素が0のタプルだそうです。

また、戻り値が設定されているからと言って必ず受け取る必要はありません。

func printAndCount(stringToPrint: String) -> Int {
    println(stringToPrint)
    return countElements(stringToPrint)
}
func printWithoutCounting(stringToPrint: String) {
    printAndCount(stringToPrint)
}
printAndCount("hello, world")
// prints "hello, world" and returns a value of 12
printWithoutCounting("hello, world")
// prints "hello, world" but does not return a value

複数の戻り値を返すことも出来ると豪語していますが、要はタプルだって戻り値に出来ますよって話です。

func count(string: String) -> (vowels: Int, consonants: Int, others: Int) {
    var vowels = 0, consonants = 0, others = 0
    for character in string {
        switch String(character).lowercaseString {
        case "a", "e", "i", "o", "u":
            ++vowels
        case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
        "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
            ++consonants
        default:
            ++others
        }
    }
    return (vowels, consonants, others)
}

返したタプルの各要素についている名前は呼び出し元のスコープで引き継ぐことが出来ます。

let total = count("some arbitrary string!")
println("\(total.vowels) vowels and \(total.consonants) consonants")
// prints "6 vowels and 13 consonants"

Function Parameter Names

引数には外部に公開する名前を設定することが出来ます。

func someFunction(externalParameterName localParameterName: Int) {
    // function body goes here, and can use localParameterName
    // to refer to the argument value for that parameter
}

「externalParameterName」が外部に公開する名前です。流石にこれだけの説明じゃ意味不明だと思うので、例を出すとこんな感じです。

func join(string s1: String, toString s2: String, withJoiner joiner: String)
    -> String {
        return s1 + joiner + s2
}

join(string: "hello", toString: "world", withJoiner: ", ")
// returns "hello, world"

外部引数名が設定されている関数を呼び出すときは、呼び出し元も必ずその名称を記述する必要があります。

外部引数名と内部引数名が一緒で問題ない場合は引数名の先頭に「#」を付与することで省略出来ます。

func containsCharacter(#string: String, #characterToFind: Character) -> Bool {
    for character in string {
        if character == characterToFind {
            return true
        }
    }
    return false
}

let containsAVee = containsCharacter(string: "aardvark", characterToFind: "v")
// containsAVee equals true, because "aardvark" contains a "v"

この機能、引数の順番を変えられるとか、カリー化が簡単に出来るとか、そんなことは一切書かれていません。むしろそれが出来たら最高にクールだったと思うんですが。

気を取り直して次に行きましょう。引数のデフォルト値を設定することが出来ます。

func join(string s1: String, toString s2: String,
    withJoiner joiner: String = " ") -> String {
    return s1 + joiner + s2
}

join(string: "hello", toString: "world", withJoiner: "-")
// returns "hello-world"

// デフォルト値が設定されている引数は省略可能
join(string: "hello", toString: "world")
// returns "hello world"

外部引数名はデフォルト値が設定されてる引数につけてあげるとジェントルだよ、とのことです。まぁそれ以外でつけるメリットマジでなさそうだしね。

func join(s1: String, s2: String, joiner: String = " ") -> String {
    return s1 + joiner + s2
}

join("hello", "world", joiner: "-")
// returns "hello-world"

また、可変長引数もサポートされています。

func arithmeticMean(numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }

    return total / Double(numbers.count)
}

arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8, 19)
// returns 10.0, which is the arithmetic mean of these three numbers

Swiftの引数は基本的に値を置き換えたりすることは出来ません。引数の値を操作したい場合は、引数名の前にvarをつける必要があります。

func alignRight(var string: String, count: Int, pad: Character) -> String {
    let amountToPad = count - countElements(string)
    for _ in 1...amountToPad {
        string = pad + string
    }
    return string
}

let originalString = "hello"
let paddedString = alignRight(originalString, 10, "-")
// paddedString is equal to "-----hello"
// originalString is still equal to "hello"

単に関数内で引数を操作出来るだけで、俗に言う「参照渡し」ではないです。

参照渡しのような動作を期待する場合は引数名の前にinoutをつけます。

inout引数には制約として「デフォルト値を与えられない」「可変長引数を受け付けない」と言うものがありますが、まぁそんなもん求めることはまずないでしょう。

func swapTwoInts(inout a: Int, inout b: Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107

// inout引数が設定されている関数を呼び出す時は変数の前に"&"をつけること
swapTwoInts(&someInt, &anotherInt)
println("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"

Function Types

Swiftの関数は所謂「ファーストオブジェクト」として扱われます。

(ここまで読んでみて、Swiftは「オブジェクト指向関数型プログラミングパラダイムを加えた」と言うよりは、「関数型プログラミングオブジェクト指向パラダイムを仕方なく追加してやった」ぐらいの立ち位置を取ってそうな気がしてきたので、「オブジェクト」と言う単語は不適切だと思っている。まァ、言葉の厳格な定義はプログラミング言語オタクの人たちに任せるとしよう。)

以下の二つ関数の型はどちらも「(Int, Int) -> Int」と言う型です。

func addTwoInts(a: Int, b: Int) -> Int {
    return a + b
}

func multiplyTwoInts(a: Int, b: Int) -> Int {
    return a * b
}

戻り値や引数を持たない関数でも当然型はあります。下の関数は「() -> ()」と言う型になります。

func printHelloWorld() {
    println("hello, world")
}

型を持っているのだから関数を変数にバインドするのは何の問題もありません。

var mathFunction: (Int, Int) -> Int = addTwoInts

println("Result: \(mathFunction(2, 3))")
// prints "Result: 5"

addTwoIntsとmultiplyTwoIntsはどちらも「(Int, Int) -> Int」と言う型を持っています。mathFunctionも「(Int, Int) -> Int」です。そして変数はvarで宣言されています。なので、型があっていれば変数に関数を再代入したって誰にも怒られません。

mathFunction = multiplyTwoInts
println("Result: \(mathFunction(2, 3))")
// prints "Result: 6"

何度でも言いますが関数は型を持っています。なので関数を引数で渡したって文句を言われる筋合いはありません。

func printMathResult(mathFunction: (Int, Int) -> Int, a: Int, b: Int) {
    println("Result: \(mathFunction(a, b))")
}

printMathResult(addTwoInts, 3, 5)
// prints "Result: 8"

当然戻り値にしてもいいでしょう。

func stepForward(input: Int) -> Int {
    return input + 1
}
func stepBackward(input: Int) -> Int {
    return input - 1
}

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
    return backwards ? stepBackward : stepForward
}

var currentValue = 3
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function


println("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
println("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
println("zero!")
// 3...
// 2...
// 1...
// zero!

Nested Functions

グローバルなスコープ(どこ?)で宣言された関数はすべてグローバルな関数になります。

関数の中で関数を定義することで、スコープを制限することが出来ます。スコープ外から呼び出すことが出来なくなるだけで、参照自体は渡すことが出来ます。

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backwards ? stepBackward : stepForward
}

var currentValue = -4
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function

while currentValue != 0 {
    println("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}

println("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!