[Swift] 関数における引数/戻り値とタプルの関係

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Swift においてタプルはパターンマッチが手厚くサポートされているなど、Optional 同様に特別な扱いを受けています。 関数とタプルについて調べてみましたが、いまいち結論が曖昧な部分が多かったです。 しかし、関数とタプルがどういう関係であるのか見えてくる部分もありましたので、記事として残しておきます。

戻り値

戻り値と空タプル

Swift では、戻り値を返さない関数の型は T -> Void 型で表現されます。

func test(str: String) {
    println(str)
}

let f: String -> Void = test

ここで出てくる Void 型は () 型の型エイリアスで、Swift のモジュール内で定義されています。

typealias Void = ()

() 型というのは空のタプルを表す型です。 つまり、戻り値を返さないと定義された関数も、実際には空タプルを返しています。 また、このことは Swift の公式ドキュメントにも明記されています。

func test2() {
}

let a = test2()
let b: () = test2()
let c: Void = test2()

戻り値とタプル

Swift では、単一の要素を持つタプル (T) 型は T 型とみなされると公式ドキュメントに記述されています。 また、単一の戻り値を持つ関数については、戻り値の型をタプルで囲っても問題なくコンパイルが通ります。

// OK
func test3() -> (Int) {
    return 1
}

let f7: () -> Int = test3 // () -> (Int) と同等

加えて、戻り値が複数の値から構成される場合にもまたタプルが返されることから、Swift においては全ての戻り値がタプルとして扱われているであろうということが推測できます。

引数

引数と空タプル

Swift では、引数を取らない関数の型は () -> T 型で表現されます。

func test() -> String {
    return "aaa"
}

let f: () -> String = test

この型は、@autoclosure ディレクティブを利用した値の遅延評価の際によく利用されます。 組み込み関数である assert 関数などが代表例です。

() -> T 型は () 型を引数に取って T 型を返す関数の型と読むことができます。 実際にその通りで、パラメータの数が0個として定義された関数は、空のタプルを引数として受け取るようになっています。 つまり、Swift においては、戻り値と同様に全ての関数が何らかの引数を取ることになります。

また、空タプルを引数として渡してもコンパイルエラーは発生しません。

func test4() -> Int {
    return 1
}

test4() // 1
test4(()) // 1
test4(Void()) // 1

なお、試しに空タプルを引数として取る関数を定義したところ、その関数は引数を明示的に渡すことなしに実行することができました。 関数を実行する際にパラメータを渡さないと、暗黙的に空タプルが渡されるようになっているようです。

func test5(a: ()) -> Int {
    return 1
}

// 引数なしで実行可能
test5() // 1
test5(()) // 1
test5(Void()) // 1

関数の引数としてタプルを渡す

Swift の関数はパラメータリストに対応したタプルを引数として渡すことができます。

func test(a: String, b: Int) -> (Int, String) {
    return (b, a)
}

let tuple = ("aaa", 1)
test(tuple) // (1, "aaa")

しかし、パラメータを最初からタプルとして定義した場合には、引数を個別に渡すことはできませんでした。

func test2(args: (String, Int)) -> (Int, String) {
    let (a, b) = args
    return (b, a)
}

test2("aaa", 1) // コンパイルエラー (Extra argument in call)
test2(tuple) // (1, "aaa")

先程の2つの関数を (String, Int) -> (Int, String) 型の変数に格納すると、どちらの関数も同じ動作をするようになります。 引数をパラメータリストに対応した個別の値として渡すことも、パラメータリストに対応したタプルとして渡すことも可能です。

let f1: (String, Int) -> (Int, String) = test

f1("aaa", 1) // (1, "aaa")
f1(tuple) // (1, "aaa")

let f2: (String, Int) -> (Int, String) = test2

f2("aaa", 1) // (1, "aaa")
f2(tuple) // (1, "aaa")

関数の引数の型を、(String, Int) 型を要素として、その外部名を省略したタプルとしてみたところ、test2 関数と同様の挙動をするようになりました。 test2 関数は (_: (String, Int)) -> (Int, String) 型、もしくはそれに準ずる型として扱われているようです。

let f3: (_: (String, Int)) -> (Int, String) = test2

f3("aaa", 1) // コンパイルエラー (Extra argument in call)
f3(tuple) // (1, "aaa")

これらのことから推測できるのは、test 関数と test2 関数は内部的には同等のものとして扱われているが、引数の数のチェックは関数の型に基づいて行われているようだということです。 複数のパラメータを持つ関数にタプルを引数として渡すことができますが、これはパラメータリストに対して引数のタプルをパターンマッチさせて、個々のパラメータに値をバインドしているように見えます。 関数の引数として単一のタプルが渡された場合には、引数の数のチェックをせずにパラメータへの値のバインドを試みるといったルールが存在しているのかもしれません。

関数の型とパラメータ

関数の代わりにクロージャリテラルで定義しても同じです。

let f3: (String, Int) -> (Int, String) = { a, b in (b, a) }

let f4: (String, Int) -> (Int, String) = { args in
    let (a, b) = args
    return (b, a)
}

f3("aaa", 1) // (1, "aaa")
f3(tuple) // (1, "aaa")
f4("aaa", 1) // (1, "aaa")
f4(tuple) // (1, "aaa")

ここで重要なことは、f3f4(String, Int) -> (Int, String) 型の関数として扱われているということです。 これは、パラメータが複数ある関数の型は、パラメータを個別に受け取る関数でもタプルで受け取る関数でも定義可能であるということを示しています。

下記コードの関数 test3 は、(String, Int) -> (Int, String) 型の関数を引数に取って (Int, String) 型のタプルを返す ((String, Int) -> (Int, String)) -> (Int, String) 型の関数です。 test3 の引数として渡す関数は、パラメータをタプルで受け取ることが可能です。

func test3(f: (String, Int) -> (Int, String)) -> (Int, String) {
    return f("aaa", 1)
}

test3 { a, b in (b, a) } // (1, "aaa")

test3 { args in
    let (a, b) = args
    return (b, a)
} // (1, "aaa")

ハンドラのパラメータをタプルで受け取る

関数のパラメータをタプルで受け取ると便利なことがあります。その例を見てみましょう。

下記のコードは、NSURLConnectionsendAsynchronousRequest:queue:completionHandler: メソッドのハンドラをクロージャリテラルで定義している例です。 結果がエラーであるかの判定を行うとともに、各パラメータの Optional の unwrap やキャストを行っています。 この処理を簡潔に記述するために、パラメータをタプルにした上で switch 文に渡し、パターンマッチによる値の分解を行っています。

let url = NSURL(string: "http://www.google.co.jp")!
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "GET"

NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response, data, error) in
    switch (response, data, error) {
    case let (_, _, .Some(error)):
        // ...
    case let (.Some(response as NSHTTPURLResponse), .Some(data), _):
        // ...
    default:
        // ...
    }
}

sendAsynchronousRequest:queue:completionHandler: メソッドのハンドラは、(NSURLResponse!, NSData!, NSError!) -> Void 型です。 先程、パラメータが複数ある関数は、パラメータを個別に受け取ることもタプルで受け取ることもできるということが分かりました。 このことから、sendAsynchronousRequest:queue:completionHandler: メソッドにおけるハンドラのパラメータは、(NSURLResponse!, NSData!, NSError!) 型のタプルで受け取ることが可能であることが分かります。 上記コードは下記のように書き直すことが可能です。

NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { result in
    switch result {
    case let (_, _, .Some(error)):
        // ...
    case let (.Some(response as NSHTTPURLResponse), .Some(data), _):
        // ...
    default:
        // ...
    }
}

最初のコードでは、ハンドラに渡される3つのパラメータをタプル化して switch 文に渡していました。 しかし、ハンドラのパラメータを最初からタプルとして受け取ることで、いちいち全てのパラメータを記述する必要がなくなっています。

iOS SDK で提供される API では、ハンドラに渡されるパラメータが結果データとエラーを含んだ複数の Optional であるケースが多いです。 このため、パラメータをタプルとして受け取ってそのまま switch 文に渡すというパターンを覚えておくと便利かと思います。

まとめ

今回分かったことは下記の通りです。

  • Swift の関数は、必ず何らかの引数を取り、何らかの戻り値を返す
  • 引数と戻り値のほとんどはタプルで構成されており、そうでないものもタプルと互換性があるような仕組みになっている

これらのことが分かったからといって一見あまり役に立たなそうに思うかもしれません。 しかし、関数を中心に処理を組み立てる際には、個々の関数の入力と出力の型を把握しておくことはとても重要ですので、頭の片隅に入れておきたいところです。