Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 362 - Swift 3.0 : 関数(Functions)

2017年2月2日:パラメータの定義を修正:2016年9月8日:Xcode 8 GM seedの公開に併せて改*1

Function(関数)は、コーディングの要になります。特定の働きを定義した関数を作って名前をつけます。そして、その名前を呼ぶ(Call)ことで、目的の働きが実行されます。

すべての関数に「」があるというのも重要です。関数に定義されているパラメータ*2の型とreturnで返される戻り値の型の組み合わせが「関数の型」になるんです。

ということは、関数そのものを引数として他の関数に渡すことができるということで、更に関数をreturnで返すこともできます。

関数の中に関数を埋め込むこともできるので、関数の扱いは実に奥深いんです。

関数を扱うとき、「パラメータ」という言葉がよく使われます。しかし、「詳解 Swift 改訂版」を読んでいるとパラメータという言葉は出てきません。実は「仮引数(かりひきすう)」と訳されているんですが、このブログでは「パラメータ」とい言葉をそのまま使っていきます。

f:id:yataiblue:20151231144735j:plain 引数の定義

関数は、その働きが明示的に理解できる「関数名」が付けられ、外部から取り込む「値」を「引数(ひきすう)」と呼ぶのですが、関数からみれば「Parameters/パラメータ(仮引数)」であり、使用時に、このパラメータに渡される値のことを「Arguments/実引数」と呼びます。

Swiftでは、仮引数にわかりやすい名前をラベルとしてつけることができます。それを「引数ラベル」と呼びます。

関数が「型」のある値を返す場合、それには「Return Type/戻り値の型」がつきます。

私は混乱したんですが、Parameters(仮引数)とArguments(実引数)は同じところを指しています。

まず戻り値の無い簡単な関数から説明します。

func sendMessage() {
    let message = "Hi There!"
    print(message)
}

sendMessage()
// "Hi There!"

基本的な説明をすると

  1. func」キーワードを使って関数を定義します。
  2. 関数名(sendMessage)」を付けます。
  3. ( )」は何も引数が要らず実行されるということです。
  4. 関数の内容は「{ }」内に記述して実行されます。

次に1つだけパラメーターを持つ関数に変えます。

func sendMessage(shouting: Bool) {
    var message = "Hi There!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage(shouting: true)
// "HI THERE!"
  1. 引数を取る関数の場合「( )」内にパラメーター書きます。「:(コロン)」を付けって型を明示させます。
  2. この関数の利用時に必ず引数にパラメーター名をラベルとして付けます*3

この関数を拡張していきます。このメッセージを送る相手をパラメーターとして加えます。

func sendMessage(recipient: String, 
                 shouting: Bool) {
    var message = "Hi There, \(recipient)!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage(recipient: "Yuji", shouting: false)
// "Hi There, Yuji!"

関数を呼ぶ時に重要なポイントの1つが「英文の自然さ」です。この関数の場合、「sendmessage recipient Yuji」と並び、不自然になります。「sendMessage to Yuji」にすることができます。外部パラメータ名を使って次のように変更させることができます。

func sendMessage(to recipient: String, 
                 shouting: Bool) {
    var message = "Hi There, \(recipient)!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage(to: "Yuji", shouting: false)
// "Hi There, Yuji!"

次にカスタムメッセージが送れるように第3のパラメータを与えます。

func sendMessage(message: String, 
                 to recipient: String, 
                 shouting: Bool) {
    var message = "\(message), \(recipient)!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage(message: "See You", to: "Yuji", shouting: false)
// "See You, Yuji!"

この関数を呼ぶ時の問題は「sendMessage message」と続く英文の不自然さです。パラメータ名と関数名が重なっているので外部パラメータに「_」をつけて省略させられます。

func sendMessage(_ message: String, 
                 to recipient: String, 
                 shouting: Bool) {
    var message = "\(message), \(recipient)!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage("See You", to: "Yuji", shouting: false)
// "See You, Yuji!"

かなり自然な関数の呼び方に変わりました。しかし、滅多に「叫ぶ」(shouting: true)ことは無いので、デフォルトで「falese」にすることもできます。

func sendMessage(_ message: String, 
                 to recipient: String, 
                 shouting: Bool = false) {
    var message = "\(message), \(recipient)!"
    if shouting {
        message = message.uppercased()
    }
    print(message)
}

sendMessage("See You", to: "Yuji")
// "See You, Yuji!"

ここまでがパラメータの取り扱いのバリエーションの説明です。ここから戻り値(Return Value)を持つ関数の取り扱いの説明をします。

func firstString(havingPrefix prefix: String, 
                 in strings: [String]) -> String {
    for string in strings {
        if string.hasPrefix(prefix) {
            return string
        }
    }
    return ""
}

この関数の戻り値は「String」型です。しかし、この場合探しているprefixを持った項目を持っていない場合があり、その場合「""」空白を返します。しかし、Swift的にこれは好ましくありません。ここで重要なのが「?」オプショナル型です。

func firstString(havingPrefix prefix: String, 
                 in strings: [String]) -> String? {
    for string in strings {
        if string.hasPrefix(prefix) {
            return string
        }
    }
    return nil
}

次に関数も型の1つという説明です。

関数は次のような2つのタイプがあります

  1. () -> Void
  2. (parameter: type) -> return type

パラメータを持たない関数と持つ関数です。
「func firstString(havingPrefix prefix: String, in strings: [String]) -> String? {}」の型を表現すると、「(String, Array<String>) -> Optional<String>型」になります。

Swift3で関数のリファレンスを使用する場合、引数のラベルを明示する必要はありません。例えば上記の関数のリファレンスを下記の様に指定した場合

let s = firstString(havingPrefix:in:)

このリファレンス使用時に引数のラベル名を入れる必要はないんです。

s("Yuji", ["Yuji", "is", "a", "man"])
// Yuji

関数をパラメータとして与えることも出来ます。例えば以下のような関数です。

func filterInts(_ numbers: [Int], 
        _ includeNumber: (Int) -> Bool) -> [Int] {
    var result: [Int] = []
    for number in numbers {
        if includeNumber(number) {
            result.append(number)
        }
    }
    return result
}

パラメータとして与える関数の条件にマッチする数をフィルタリングして抽出する関数ですが、これを次のように呼びます。

var numbers = [4, 17, 34, 41, 82]
func divisibleByTwo(_ number: Int) -> Bool {
    return number % 2 == 0
}

let evenNumbers = filterInts(numbers, divisibleByTwo)
// [4, 34, 82]

実数とクロージャを与えることもできます。

let alteredNumbers = filterInts([11, 12, 23, 45, 56, 68], 
        {(number: Int) -> Bool in return number % 2 == 0})

そしてこのクロージャーの面白いところがドンドン省略できるところです。

let alteredNumbers = filterInts([11, 12, 23, 45, 56, 68], 
    {number in number % 2 == 0})

ここまで省力した形は、iOSAPI利用でよく見ます。更にパラメータも「$0」を使って省略できます。するとクロージャの宣言になる「in」も省力できます。

let alteredNumbers = filterInts([11, 12, 23, 45, 56, 68], 
    {$0 % 2 == 0})

そして最後にトレーリングクロージャにするのが一般的です。

let alteredNumbers = 
    filterInts([11, 12, 23, 45, 56, 68]) {$0 % 2 == 0}

ここからSwift3で関数の色々なパターンをみていきます。

func extractMiniMax(numbers: Double...) -> 
        (min: Double, max: Double)? {
    if numbers.isEmpty { return nil }
    
    var minNumber = numbers[0]
    var maxNumber = numbers[0]
    
    for number in numbers {
        if number < minNumber {
            minNumber = number
            
        } else if number > maxNumber {
            maxNumber = number
        }
    }
    return (minNumber, maxNumber)
}

if let someNumbers = 
  extractMiniMax(numbers: 13, 25, 4, 654, 4312, 435, 523, 64, 123) {
    print("大きい数は\(someNumbers.max)で、
          小さい数は\(someNumbers.min)です。")
}
// 大きい数は4312.0で、小さい数は4.0です。

パラメータで取り込まれる値は基本的に定数(constant)扱いで、関数内でも変更できませんが、関数内に変数(var)を指定して変更を加えることができます。

func alignRight(string: String, 
       totalLength: Int, pad: Character = "_") -> String {
    var string = string
    // swift3から関数内で「var」が利用できます。

    let amountTotalPad = totalLength - string.characters.count
    
    if amountTotalPad < 1 {
        return string
    }
    
    let padString = String(pad)
    for _ in 1...amountTotalPad {
        string = padString + string
    }
    return string
}

var originalString = "Hello"

let paddedString = 
  alignRight(string: originalString, totalLength: 10, pad: "-")
// "-----Hello"
// 戻り値の変数stringは変更が加わるので、paddedStringは"-----Hello"

originalString
// "Hello"
// しかし、元の引数として渡したoriginalStringは変更されません。

引数として渡す変数にまで変更を加える方法があります。関数内で変数「var」キーワードを使用するのではなく「inout」キーワードと「&」を使用します。「inout」キーワードがあれば引数は必ず変数になるので注意が必要です。もう一つ、関数を使用する時に引数の前に「&」を加える必要があります。

func alignRight(string: inout String, 
      totalLength: Int, pad: Character = "_") -> String {
// inoutがついている最初の引数は必ず変数になります。
    let amountTotalPad = totalLength - string.characters.count
    
    if amountTotalPad < 1 {
        return string
    }
    
    let padString = String(pad)
    for _ in 1...amountTotalPad {
        string = padString + string
    }
    return string
}

var originalString = "Hello"

let padedString = 
  alignRight(string: &originalString, totalLength: 10, pad: "-")
// 引数の変数に「&」を加えます。
// "-----Hello"
// 戻り値の変数stringは変更が加わっているので、"-----Hello"

originalString
// "-----Hello"
// inoutパラメータで指定されていたので、元の変数も変更されました。

関数に型があり、その関数をパラメータとして指定できるし、戻り値として指定できるという説明に、関数のスコープという話で締めくくります。

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
// この関数の型は「(boolean) -> (Int) -> (Int)型です。
// (Int) -> Int型の関数を戻しています。

    func stepForward(input: Int) -> Int {
        return input + 1
    }
// これが関数内にネストされている関数です。
// この関数も外部スコープから使用できるので下に記述します。

    func stepBacward(input: Int) -> Int {
        return input - 1
    }
    return backwards ? stepBacward : stepForward
}

var currentValue = -4

let moveNearerToZero = chooseStepFunction(backwards: currentValue > 0)
// この時点で、moveNearerToZeroは関数「stepForward()」になっています。

while currentValue != 0 {
    print("\(currentValue)")
    currentValue = moveNearerToZero(currentValue)
}
// -4, -3, -2, -1

let testNumber = 6
chooseStepFunction(backwards: testNumber > 0)(5)
// 4
// ネストされた関数を使用するためにchooseStepFunction()に評価を与えれば
// stepBackword()に変更されるので、Int型の引数を与えるとInt型が戻ります。

これでSwift3に関する関数は一通り網羅できました。

*1:2016年6月19日:Swift3β発表に合わせて改訂、2016年1月24日:The Swift Programming Language (Swift2 Prerelease)が発表されて。「詳解 Swift 改訂版」を参考に内容改訂

*2:詳解 Swift 改訂版」の説明で、Parameter(パラメータ)を「仮引数」と訳して使用しています。

*3:Swift3から変更がありました。Swiftの初期の頃は、Objective-Cと切っても切れない関係を重視していたので、Swit2まではObjective-Cの記述法と統一していました。しかし、Swift3では更にSwiftらしさを推し進めています。iOSフレームワークAPIもかなり改訂が見られます。