本日(2020年3月25日、現地時間では24日) Swift 5.2 がリリースされました。
Swift 5.2 がフォーカスしているのは、コード補完やエラーメッセージの改善など、開発者の UX 改善で、言語仕様に加えられた変更は多くありません。 UX の改善点については公式ブログが詳しく解説しているので、本投稿では Swift 5.2 における言語仕様の変更点について紹介します。
Swift 5.2 で言語について加えられた変更は次の二つです。
SE-0249: Key Path Expressions as Functions
Key Path 式を関数として渡せるようにする変更です。
たとえば、 User
の Array
から ID の Array
を作りたいときには、これまでは次のように書けました。
// BEFORE
let users: [User] = ...
let ids: [String] = users.map { $0.id }
Key Path 式を関数として渡せると、次のように書けるようになります。
// AFTER
let users: [User] = ...
let ids: [String] = users.map(\.id)
また、関数型の変数に Key Path 式を代入することもできるようになりました。
let getUserId: (User) -> (String) = \.id
しかし、これらは暗黙的変換であり、 Key Path 式自体が関数にように使えるわけではありません。
let id: String = \.id(user) // ⛔ NG
これを Key Path 式でやりたければ次のように書く必要があります。
let id: String = user[keyPath: \.id] // ✅ OK
なお、注意が必要なのは、 SE-0249 が対象としているのは Key Path 式だけで、 KeyPath
インスタンスを関数型に暗黙的変換できるわけではないことです。たとえば、↓はできません。
let keyPath: KeyPath<User, String> = \.id
let ids: [String] = users.map(keyPath) // ⛔ NG
SE-0253: Callable values of user-defined nominal types
struct
や class
などの nominal type を関数のように使えるようにする変更です。
たとえば、一次関数 $ax + b$ を表す型を作りたいとします。一次関数を関数ではなく nominal type として実装したい理由の例としては、 a
や b
をプロパティで持つようにして、二直線の交点を求めるメソッドを作ったりしたいなどが考えられます1。しかし、一次関数型インスタンスは f(5)
のように関数として使いたい気持ちにもなります。これまではそれらを両立できませんでした。
一次関数を Linear
という型で表すとします。これまでは次のように書く必要がありました。
// BEFORE
let f = Linear(a: 2, b: 3) // 一次関数 2x + 3
print(f.evaluate(5)) // x = 5 を代入
Swift 5.2 からは、関数のように呼び出すことが可能になります。
// AFTER
let f = Linear(a: 2, b: 3) // 一次関数 2x + 3
print(f(5)) // x = 5 を代入
上記のように Linear
を関数として使えるようにするには、 Linear
に callAsFunction
という名前のメソッドを実装します。
extension Linear {
func callAsFunction(_ x: Double) -> Double {
a * x + b
}
}
callAsFunction
メソッドを持つ型のインスタンスは何でも、関数のように呼び出せます。
しかし、 callAsFunction
を持つインスタンスを関数型に暗黙的変換することはできないので注意が必要です。
let g: (Double) -> Double = f // ⛔ NG
関数型に変換するには明示的に callAsFunction
メソッドを指定します。
let g: (Double) -> Double = f.callAsFunction // ✅ OK
なお、ここでは一次関数型を例に挙げましたが、 Proposal にはその他にも callAsFunction
の使いどころが色々挙げられています。
個人的に違和感を感じるところ
SE-0249 と SE-0253 は整合性のバランスが微妙に思えます。
関数のように使えない Key Path 式は関数型に暗黙的変換できるのに、
// 関数のように使う
//let id: String = \.id(user) // ⛔ NG
let id: String = user[keyPath: \.id] // ✅ OK
// 関数型へ変換
let getUserId: (User) -> (String) = \.id // ✅ OK
関数のように使える callAsFunction
を持つインスタンスは関数型に暗黙的変換できません。
// 関数のように使う
print(f(5)) // ✅ OK
// 関数型へ変換
let g: (Double) -> Double = f // ⛔ NG
let g: (Double) -> Double = f.callAsFunction // ✅ OK
SE-0253 の Future directions には Implicit conversions to function が書かれているので、 callAsFunction
を持つインスタンスの暗黙的変換は将来的にサポートされるかもしれません。ただ、そうすると SE-0249 は KeyPath
に callAsFunction
を持たせるだけで良かったんじゃないかとも思います( Key Path 式だけでなく KeyPath
インスタンスも関数型に暗黙的変換できるようになるのと、 KeyPath
インスタンスを関数のように使えるようになるという違いがあるので、それらが望ましいかどうかの検討は必要ですが)。
まとめ
- Swift 5.2 は開発者の UX 改善が中心で言語仕様の変更は少ない。
- 言語仕様の変更に関する Proposal は SE-0249 と SE-0253 の二つ。
- SE-0249 で Key Path 式が関数に暗黙的変換できるようになる。
- 例: BEFORE
users.map { $0.id }
→ AFTERusers.map(\.id)
- 例: BEFORE
- SE-0253 で
callAsFunction
メソッドを持つ型のインスタンスを関数のように使えるようになる。- 例: BEFORE
f.evaluate(5)
→ AFTERf(5)
- 例: BEFORE
- Swift 5.2 時点では、関数のように使えない Key Path 式は関数型に暗黙的変換できるのに、関数のように使える
callAsFunction
を持つインスタンスは関数型に暗黙的変換できないことに筆者は違和感を感じる。
-
実際に直線を表す型を作るときには、 $y$ 軸に平行な直線も表現できるように $ax + by + c = 0$ の形で実装することが一般的です。 ↩