Swift Digest
SE-0110 | Swift Evolution

Distinguish between single-tuple and multiple-argument function types

Proposal
SE-0110
Authors
Vladimir S., Austin Zheng
Review Manager
Chris Lattner
Status
Implemented

01 何が問題だったのか

Swift の関数型には、「複数の引数を取る関数」と「要素が複数あるタプルひとつを引数に取る関数」という、本来は別物として扱うべき二種類があります。しかし Swift 3 より前の型システムでは、この二つが曖昧に同一視されていました。

たとえば次のコードでは、fn1fn2 はどちらも (Int, Int) -> Void 型として宣言されていますが、クロージャの引数の受け取り方が異なります。

let fn1: (Int, Int) -> Void = { x in
    // x の型は (Int, Int) というタプル
}

let fn2: (Int, Int) -> Void = { x, y in
    // x, y はそれぞれ Int
}

つまり、「2 引数を取る関数」と宣言しても、実際には「要素数 2 のタプルを 1 つ受け取る関数」としても値を代入できてしまう状態でした。これは SE-0029 で削除されたタプルスプラット(tuple splat)の名残で、次のような問題を引き起こしていました。

  • 型として同じシグネチャを名乗っていても、呼び出し側・実装側で引数の受け取り方が食い違う余地があり、type safety が弱かった
  • 一方で ((Int, Int)) -> Int のように「タプルひとつを引数に取る関数」を素直に書く構文も整理されておらず、意図を明確に表現しにくい

要するに、型システム上は「n 個の引数を取る関数」と「要素数 n のタプル 1 個を取る関数」を明確に区別したいのに、Swift の既存の挙動がそれを許していない、というのが問題でした。

一方で、これを単純に厳格化すると、タプルの並びに対してそのまま多引数の関数値を渡せる次のような実用的なパターンが壊れてしまう、という実務上の懸念もありました。

zip([1, 2, 3], [3, 2, 1]).filter(<)   // => [(1, 3)]
zip([1, 2, 3], [3, 2, 1]).map(+)      // => [4, 4, 4]

zip の結果はタプルの並びなので、filtermap に渡されるクロージャの引数型は「タプルひとつ」になりますが、ユーザーとしては <+ のような多引数の関数をそのまま渡したいところです。型区別を厳格化しつつ、こうした書き味も維持することが求められていました。

02 どのように解決されるのか

この提案では、関数型において「複数引数」と「単一のタプル引数」を型システム上はっきり区別します。

複数引数の関数型には複数引数の値だけを渡せる

n > 1 個の引数を持つ関数型(たとえば (Int, Int) -> Void)を満たせるのは、同じ個数の引数を取る関数値だけです。タプルひとつを受け取るクロージャは受け付けません。

// OK: 2 引数を取るクロージャ
let fn: (Int, Int) -> Void = { x, y in
    print(x, y)
}

// error: タプルひとつで受け取るのは不可
let bad: (Int, Int) -> Void = { x in
    print(x)
}

「タプルひとつを引数に取る関数」は二重括弧で書く

要素数 n > 1 のタプルひとつを引数に取る関数型を宣言したいときは、引数リストの外側をもう一段括弧で囲みます。

let a: ((Int, Int, Int)) -> Int = { x in
    return x.0 + x.1 + x.2
}

a((1, 2, 3))   // 6

Swift の従来の規約では「単一の対象を包む丸括弧は意味を持たない」とされていましたが、ここではその例外として、((...)) -> V を「タプル引数ひとつを取る関数型」専用の表記として使い分けます。関数型における一度きりの特例という位置付けです。

実用のための救済: (T, U, ...) -> V から ((T, U, ...)) -> V への変換

これだけでは、zip のようなタプル列に対する高階関数に多引数関数を直接渡す書き方が壊れてしまいます。そこで、関数呼び出しの引数として関数値を渡す場面に限り(T, U, ...) -> V 型の値を対応する ((T, U, ...)) -> V 型のパラメータに自動変換することを許容します。

この救済のおかげで、次のような典型的なコレクション処理はこれまでどおり書けます。

zip([1, 2, 3], [3, 2, 1]).filter(<)   // => [(1, 3)]
zip([1, 2, 3], [3, 2, 1]).map(+)      // => [4, 4, 4]

filtermap は本来「タプルひとつを受け取るクロージャ」を要求しますが、<+ のような多引数の関数値も、呼び出し時の引数としてならそのまま渡せる、という形です。

利用者が押さえておくべきこと

  • 複数引数の関数型の変数に、タプルひとつ受け取りのクロージャをそのまま代入することはできなくなりました。明示的にタプルを分解(destructure)するか、型を ((T, U, ...)) -> V の形に書き直す必要があります。
  • 逆に「タプルひとつを引数に取る関数型」を扱いたいときは、((T, U, ...)) -> V と二重括弧で書くのが基本形です。
  • 高階関数の呼び出しで多引数関数を渡す書き方(zip(...).map(+) など)は、上記の救済ルールによって引き続き書けます。