Swift Digest
SE-0111 | Swift Evolution

Remove type system significance of function argument labels

Proposal
SE-0111
Authors
Austin Zheng
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 3以前では、関数の引数ラベル(argument label)が関数の型の一部として扱われていました。つまり、(x: Int, y: Int) -> Bool(Int, Int) -> Bool、あるいは (a: Int, b: Int) -> Bool(x: Int, y: Int) -> Bool はすべて異なる型であり、型システムはそれらのあいだに暗黙の変換関係(サブタイプ関係)を張っていました。

func doSomething(x: Int, y: Int) -> Bool {
    return x == y
}

let fn1: (Int, Int) -> Bool = doSomething
// OK
fn1(1, 2)

// fn2 の型は (x: Int, y: Int) -> Bool
let fn2 = doSomething

// OK
fn2(x: 1, y: 2)
// エラー: ラベルなしでは呼べない
fn2(1, 2)

この挙動は型システムを複雑にするだけでなく、意味のない、あるいは誤解を招くコードを通してしまう原因にもなっていました。たとえば次のコードでは、battingAveragePredicate に見た目とまったく異なる関数 sinkBattleship を代入でき、呼び出し時に ofHits: / forRuns: というラベルがあたかも意味を持つかのように見えてしまいます。

func sinkBattleship(atX x: Int, y: Int) -> Bool { /* ... */ }
func meetsBattingAverage(ofHits hits: Int, forRuns runs: Int) -> Bool { /* ... */ }

var battingAveragePredicate: (ofHits: Int, forRuns: Int) -> Bool = meetsBattingAverage
battingAveragePredicate = sinkBattleship

// 実際に呼ばれるのは sinkBattleship
battingAveragePredicate(ofHits: 1, forRuns: 2)

引数ラベルは本来、宣言(declaration)に紐付く「呼び出し側の可読性のための名前」であり、デフォルト引数と同様に型そのものの一部ではありません。それにもかかわらず型に引きずり込まれていたため、型システムが不必要に複雑化し、上のような直感に反する挙動が生じていました。

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

関数型(function type)から引数ラベルの意味を取り除き、関数型は仮引数の型と戻り値の型のみで定義されるように変更します。引数ラベルは宣言(関数・メソッド・イニシャライザ)の名前の一部としてのみ意味を持ち、型システムには現れません。

関数型の記述

関数型を書くときにラベルを付けることはできなくなり、型は純粋に型の並びと戻り値だけで表します。

func doSomething(x: Int, y: Int) -> Bool {
    return x == y
}

func somethingElse(a: Int, b: Int) -> Bool {
    return a > b
}

// fn2 の型は (Int, Int) -> Bool
var fn2 = doSomething

// OK
fn2(1, 2)

// OK: 仮引数ラベルが異なっていても、同じ型の関数として代入できる
fn2 = somethingElse

ラベル付きの関数型を書くことは禁止されます。

// エラー
let fn3: (a: Int, b: Int) -> Bool

// 次のように書く必要があります
// let fn3: (Int, Int) -> Bool

呼び出し時にラベルが必要かどうかの判断

ラベルが必要になるかどうかは、呼び出しが「宣言そのものを指しているか」「関数型の値を指しているか」で決まります。これはデフォルト引数と同じルールです。

  • 宣言名そのものを通じて呼び出す場合は、ラベルが必要です。引数リストに書くか、参照時のフルネームに含めます。

      func doSomething(x: Int, y: Int) -> Bool { return true }
    
      // 引数リストにラベルを書く
      doSomething(x: 10, y: 10)
    
      // フルネーム(`doSomething(x:y:)`)で参照する
      doSomething(x:y:)(10, 10)
    
      // エラー: フルネーム参照と引数ラベルの両方を書くのは冗長
      // doSomething(x:y:)(x: 10, y: 10)
    
  • 関数型の値・プロパティ・変数を通じて呼び出す場合は、ラベルを書いてはいけません。書くとエラーになります。

      func doSomething(x: Int, y: Int) -> Bool { return true }
    
      let x = doSomething
    
      // OK
      x(10, 10)
    
      // エラー
      x(x: 10, y: 10)
      x(something: 10, anotherThing: 10)
    

タプルのラベルには影響しない

この変更は、関数を呼び出す際の仮引数リストに対してのみ適用されます。戻り値などに現れるラベル付きタプル型(例: func homeCoordinates() -> (x: Int, y: Int))は従来どおり扱われ、タプルのメンバ名としてのラベルは引き続き型の一部として意味を持ちます。

既存コードへの影響

関数型の変数や引数の型注釈にラベルを書いていた箇所、および関数型の値をラベル付きで呼び出していた箇所は書き換えが必要になりますが、多くの場合はラベルを削除するだけで対応できます。この変更により、型システムが単純化され、battingAveragePredicate = sinkBattleship のように見た目と意味が食い違う代入も起こらなくなります。