Swiftの関数 | 関数型(Function Types)

Swiftの関数 | 関数型(Function Types)

ここでは、関数型(Function Types)と入れ子にした関数(Nested Functions)について説明していきます。関数型とは、関数の引数と戻り値をひとまとめにした型、です。型ですから、これ自体を引数にしたり、戻り値にすることも可能です。Swiftでは、型安全が非常に厳密に適用されます。したがって、function typesを使って、実装に依らないより一般的な関数(機能)を、型安全を保ったまま作ることが可能です。

ここでは、function typesとは何か?から始めて、function typesを引数にする場合と、戻り値にする場合を説明していきます。また、関数の入れ子についても、簡単に触れたいと思います。

関数型(Function Types)

Function Typesって何?

Function types(関数型)とは、一言で言うと

関数の引数と戻り値をひっくるめた型

です。

例で見た方が分かりやすいので、前に使ったantiCommutator()を取り上げてみます。

func antiCommutator(_ firstInteger: Int, _ secondInteger: Int) -> Int {
....

この関数のfunction typesは(Int, Int) -> Intになります。日本語にすると

このfunction typeはパラメータが2つ、その型は両方Int、戻り値はIntです

となります。ややこしいですが、この全体を「型」として取り扱うのがfunction typesです。

後で使いやすいようにラベル(呼び出す時に指定するパラメータ名、英語だとargument label)をアンダースコア_にセットしています。アンダースコアをセットしておくと呼び出しの時にパラメータ名を省略出来ます。

Function typesを使う

Function typeの具体例 | 整数のべき乗を計算する関数

Function typesは、他の基本型のように、定数や変数に代入またはそれらを初期化することができます。これを説明するために演算関数を1つ用意します。

// 整数のべき乗の計算
func exponent(_ number: Int, _ power: Int) -> Int {
    var result = number
    for _ in 1..>power {
        result *= number
    }
    return result
}
print("2^10 = \(exponent(2, 10))")
//"2^10 = 1024"と表示

入力した整数のべき乗を計算する関数です。2^10は2を10個掛け算するのと同値です。

Function typeを持った変数の初期化

では、この関数のfunction typeを別の変数で初期化してみます。

// Function typesの使用例
var mathFunction: (Int, Int) -> Int = exponent

この初期化は日本語にすると、

変数mathFunctionを定義、この変数は”パラメータが2つ、その型は両方Int、戻り値はInt“というfunction typeを持ち、exponentという関数を参照しています

と読めます。

新しく作った変数mathFunctionは、関数として使用することができます。

// 整数のべき乗を計算
print("結果: \(mathFunction(2, 5))")
//"結果: 32"と表示

今、mathFunctionは関数exponentを参照しているので、上記の例は2^5 = 2*2*2*2*2を計算します。結果は32ですので、関数としてちゃんと機能しているのが分かります。

別の関数を代入してfunction typeの参照先を変更

同じfunction typeを持つ関数であれば、先ほどのmathFunctionに代入することが可能です。今、antiCommutator(Int, Int) -> Intというfunction typeなので、mathFunctionに代入することが出来ます。

// 同じfunction typeを持つantiCommutatorを代入
mathFunction = antiCommutator
print("結果: \(mathFunction(2, 5))")
//"結果: 20"と表示

全く同じパラメータを使ってmathFunctionを実行しましたが、結果はantiCommutatorからのアウトプットですから、20 = 2*5 + 5*2となります。

また、型推論はfunction typesに関しても適用されますので、以下のような初期化も可能です。

// Function typeの型推論
//   exponentのfunction typeは(Int, Int) -> Intなので、
//   anotherMathFunctionのfunction typeもそのように型推論される
let anotherMathFunction = exponent

Function typesを引数にする

Function typesは引数にすることもできます。公式マニュアルの使用例を流用します。

// Function typeをパラメータに
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("結果: \(mathFunction(a, b))")
}
printMathResult(exponent, 3, 4)
//"結果: 81"と表示

関数printMathResult()はパラメータを3つ持ってます。1つ目はfunction type(Int, Int) -> Int、2つ目と3つ目はIntです。

関数自体の機能としては、整数a, bをfunction type(Int, Int) -> Intを持つ関数に渡し、その結果をprint()関数で表示する、というものです。上記の使用例では、関数の実体としてexponentを渡していますが、同じfunction typeを持つ関数であれば、どんな関数でも入力することが可能です。

このようにfunction typeをパラメータにすると、関数の実装(implementation)に依らない、より一般的な(汎用性の高い)関数を作ることが可能になります。また、型自体は厳密に指定してありますので、型安全を保ったまま関数の一般化ができるのはうれしい仕様です。

Function typesを戻り値にする

Function typeを戻り値にすることもできます。公式マニュアルの例が分かりやすいですが、敢えて自作サンプルを使います。以下に示すのは時刻に応じて違うあいさつを返す関数です。

// Function typeを戻り値に
func sayMorning(name: String) -> String {
    return "おはよう、\(name)"
}

func sayHello(name: String) -> String {
    return "こんにちわ、\(name)"
}

func sayEvening(name: String) -> String {
    return "こんばんわ、\(name)"
}

func chooseGreeting(hour: Int) -> (String) -> String {
    var greeting = sayMorning
    switch hour%24 {
    case 0..<6:
        greeting = sayEvening
    case 6..<12:
        greeting = sayMorning
    case 12..<18:
        greeting = sayHello
    case 18...24:
        greeting = sayEvening
    default:
        break
    }
    
    return greeting
}

for hour in 0...24 {
    let sayGreeting = chooseGreeting(hour: hour)
    print("時刻は\(hour)時です| \(sayGreeting("太郎"))")
}

簡単に説明します。

朝、昼、晩用あいさつ関数

先ず、3つの簡単な関数(sayMorningsayHellosayEvening)を用意します。これは単に「"おはよう"などのあいさつ+パラメータに指定された名前」を文字列として返す関数です。

func sayMorning(name: String) -> String {
    return "おはよう、\(name)"
}

朝用だけを抜き出してきましたが、基本的な構造は全て同じで、関数名と中身の文字列が違うだけです。

switch文で条件分岐

次に関数chooseGreetingを作ります。これはfunction typeの戻り値(String) -> Stringを持ちます。この関数は、パラメータである時刻hourに応じて異なるあいさつ関数を返します。

条件分岐はswitch文で実行しており、時刻を6時間毎に分割して、それぞれの時間帯で最適と思われるあいさつ関数を実行するという構造になっています。

実際に使ってみる

最後は実際の使用例です。時刻を0...24時で1時間毎にループして、それぞれの時刻でのあいさつを返すようになっています。ループの中身を少し詳しく説明します。

// hourはInt、sayGreetingは(String) -> String
let sayGreeting = chooseGreeting(hour: hour)

先ず、chooseGreeting()関数のパラメータは定義からIntです。したがって、パラメータに時間を入れて、その戻り値をsayGreetingに入れています。

chooseGreeting()関数の戻り値は(String) -> Stringですから、sayGreeting()はパラメータがStringで戻り値がStringになるような関数です。

print("時刻は\(hour)時です| \(sayGreeting("太郎"))")

関数の実体はchooseGreeting()に指定されたhourによって異なります。

出力は載せませんでしたが、興味があれば実際にXcode上で動かして確認して見て下さい。

補足: Function typeを戻り値にする場合はラベルを付けないのが基本?

関数型を戻り値にした場合、自動的にラベルが省略される処理が付与されるようです。先程の使用例から分かると思いますが、本来

sayGreeting(name: "太郎")

と書くような気がしますが、こうするとコンパイルエラーになるようです。

そもそもラベルを付けないことを想定しているのかもしれないので、最初から

func sayMorning(_ name: String) -> String {
    ....

という具合にラベルにアンダースコアを指定しておいた方が混乱を避けられるかもしれません。

関数の入れ子(Nested Functions)

これまでに定義してきた関数は全てグローバル関数(global functions)です。グローバル関数は、プログラム内のどこでも使える関数です。一方で、使用場所を限定した関数を定義することもできます。これを入れ子にした関数(nested functions)(または関数の入れ子)と呼びます。ローカル関数とは呼ばないようですね。

一つ前の例で作った関数をnested functionsにしてみます。

// 関数の入れ子 (nested functions)
func chooseGreeting(hour: Int) -> (String) -> String {
    func sayMorning(name: String) -> String { return "おはよう、\(name)" }
    func sayHello(name: String) -> String { return "こんにちわ、\(name)" }
    func sayEvening(name: String) -> String { return "こんばんわ、\(name)" }

    var greeting = sayMorning
    switch hour%24 {
    case 0..<6:
        greeting = sayEvening
    case 6..<12:
        greeting = sayMorning
    case 12..<18:
        greeting = sayHello
    case 18...24:
        greeting = sayEvening
    default:
        break
    }

    return greeting
}

// 上記のchooseGreeting関数の使用例
for hour in 0...24 {
    let sayGreeting = chooseGreeting(hour: hour)
    print("時刻は\(hour)時です| \(sayGreeting("太郎"))")
}

入れ子にした関数は、そのスコープ内でのみ有効です(この例だと、chooseGreeting()内のみ)。したがって、グローバル関数のように、プログラム上のどこでも呼び出すことはできません。

まとめ

  • Function typesは「関数の引数と戻り値をひとまとめにした型」
  • Function typesは、他の基本型と同様に、定数・変数への型として指定することができる。また、引数や戻り値にも指定できる
  • 関数の入れ子(nested functions)を利用することで、関数のスコープを限定できる