こんにちは、AIプラットフォームClova開発チームのezuraです。この記事はLINE Advent Calendar 2017の14日目の記事です。
みなさん、今年のSwiftマイグレーション祭りは無事に終わりましたか。例年よりも穏やかなエラーを味わえたのではないでしょうか。
さて、マイグレーションといえば、みなさんの行ったマイグレーションは「普通の」マイグレーションですか?それとも「スーパー」マイグレーションですか?
ここでいう「スーパーマイグレーション」とは、エラー・警告部分を修正するだけでなく、Swiftの改善によって効率的に書けるようになった部分の変更を含めたマイグレーションです。
スーパーマイグレーションの対象箇所はエラー等が出ない部分も含むため、いざマイグレーションを始めると見つけにくいですよね。効率良く移行するには、今のうちから、新しいバージョンに移行しやすいような設計や記述にしておくことや、新規または既存のコードに今後実装される機能によって改善できる部分を見つけたらマークをつけておくなど、下準備が重要です。これはSwift Projectでもよく見られる光景です。
しかし、そのためにはこの先に何が変わるのかを早めに知っておく必要があります。
本記事では、現在(記事執筆時点:2017年11月27日)の最新バージョンであるSwift 4.0よりも先、Swift 4.1以降について紹介します(Swift 4.1に実験的に入る予定の機能も紹介していますので、「Swift 4.1+」というタイトルです)。
ちなみに、Swift 4.1は2018年の春にリリースされる予定のようです。
Equatable
/Hashable
適合による==
/hashValue
の暗黙的実装
今までは、一部を除く自作の型(structおよびassociated valueを持つenum)への==
やhashValue
の実装を自分で書く必要がありましたが、Swift 4.1からは、条件を満たせばそれが不要となります。
上がSwift 4.0までのコード、下がSwift 4.1から利用できるコードです。
struct Person: Equatable { let firstName: String let lastName: String let birthDate: Date static func == (lhs: Person, rhs: Person) -> Bool { return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.birthDate == rhs.birthDate } }
struct Person: Equatable { let firstName: String let lastName: String let birthDate: Date // 以下の記述は不要です。 // static func == (lhs: Person, rhs: Person) -> Bool { ... } }
union style enumでも同様です。
enum Token: Equatable { case string(String) case number(Int) case lparen case rparen static func == (lhs: Token, rhs: Token) -> Bool { switch (lhs, rhs) { case (.string(let lhsString), .string(let rhsString)): return lhsString == rhsString case (.number(let lhsNumber), .number(let rhsNumber)): return lhsNumber == rhsNumber case (.lparen, .lparen), (.rparen, .rparen): return true default: return false } } }
enum Token: Equatable { case string(String) case number(Int) case lparen case rparen // 以下の記述は不要です。 // static func == (lhs: Token, rhs: Token) -> Bool { ... } }
厳密には「structとassociated valueを持つenum(以下、union style enum)へのEquatable
やHashable
適合のための実装(==
/hashValue
)が、後述する条件を満たすことで暗黙的に実装される」という変更です。これ以降の節では、暗黙実装の条件や背景などの詳細を説明していきます。
Equtable
とHashable
について
最初に、Equtable
とHashable
のおさらいです(不要な方は次の節からお読みください)。
Equtable
とは、型のインスタンス同士が等しいかどうかの比較が可能であることを保証するプロトコルです。対象の型同士の等号(==
)の実装が必要です。例えば、自作の型をEqutable
に適合させるには、下記のように実装します。
struct S: Equatable { let v1, v2 : String // `Equatable`に適合させるには`==`の実装が必要です。 static func ==(lhs: S, rhs: S) -> Bool { return lhs.v1 == rhs.v1 && lhs.v2 == rhs.v2 } } let s1 = S(v1: "●", v2: "○") s1 == s1 // true
Hashable
は、ハッシュ値を提供できることを表すプロトコルです。これに適合している型はDictionary
のキーやSet
の要素として扱うことができます。var hashValue: Int { get }
の実装が必要です。
struct S: Hashable { let v1, v2 : String // `Hashable`に適合させるには`var hashValue: Int { get }`が必要です。 var hashValue: Int { return v1.hashValue ^ v2.hashValue } // `Hashable`は`Equtable`を継承していますので、`==`演算子も必要です。 // (`hashValue`の一致後に、さらに`==`で同等の値であるか評価します) static func ==(lhs: S, rhs: S) -> Bool { return lhs.v1 == rhs.v1 && lhs.v2 == rhs.v2 } } [S(v1: "●", v2: "○"): "", S(v1: "◆", v2: "◇"): ""]
Swift 4.0まで
標準ライブラリで提供されている型の多くはEquatable
とHashable
に適合していますが、自作の型の場合、==
やhashValue
の実装を自前で書く必要がありました(ただし、associated valueを持たないenumは暗黙的にEquatable
とHashable
に適合しています)。
例えば、Equatable
の適合のために==
を実装する際、structの場合は全てのプロパティに対して等しいかを、union style enumに対しては列挙子とそのassociated valueが等しいかを検査するボイラープレートを書くことが多いと思います。とても退屈な作業ですよね。書いて終わりではなく、プロパティが増えた際に追加漏れが起きないよう、注意しなくてはなりません。
struct Person: Equatable { let firstName: String let lastName: String let birthDate: Date static func == (lhs: Person, rhs: Person) -> Bool { return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.birthDate == rhs.birthDate } }
enum Token: Equatable { case string(String) case number(Int) case lparen case rparen static func == (lhs: Token, rhs: Token) -> Bool { switch (lhs, rhs) { case (.string(let lhsString), .string(let rhsString)): return lhsString == rhsString case (.number(let lhsNumber), .number(let rhsNumber)): return lhsNumber == rhsNumber case (.lparen, .lparen), (.rparen, .rparen): return true default: return false } } }
Swift 4.1から
Equatable
に適合し、後述する条件を満たした型であれば、static func == (lhs: Self, rhs: Self) -> Bool
が暗黙的に実装されます。Hashable
に適合し、後述する条件を満たした型であれば、static func == (lhs: Self, rhs: Self) -> Bool
とvar hashValue: Int { get }
が暗黙的に実装されます。
各プロトコルに必要な実装が暗黙的に実装されるには、関連している型(stored instance property、associated value)もそれぞれのプロトコルに適合している必要があります。
どちらの場合も、カスタマイズする必要があるなら、明示的に実装すればその実装が採用されます。
Equatable
の場合
- 型の宣言部に
Equatable
の適合を宣言(注:extension部での宣言には適用されない) - 下記の型も
Equatable
に適合- union style enumの場合:caseが保持する全てのassociated value
- structの場合:全てのstored instance property
- 下記の条件を満たさない
- union style enumの場合:caseがない(そもそもインスタンス化できない)
Hashable
の場合
- 型の宣言部に
Hashable
の適合を宣言(注:extension部での宣言には適用されない) - 下記の型も
Hashable
に適合- union style enumの場合:caseが保持する全てのassociated value
- structの場合:全てのstored instance property
- 下記の条件を満たさない
- union style enumの場合:caseがない(そもそもインスタンス化できない)
最近のSwiftは、CodableやKeyPathの改善など、コンパイラ側でこちらの負担を減らし、かつ、コードの整合性を維持してくれる機能が増えてとても嬉しいですね。
特定の条件下でのプロトコル適合(conditional conformances)
この機能はSwift 4.1では実験的な機能として実装され、デフォルトでは使用することはできませんが、フラグを付ければ有効になります(詳細:SE-0143 Put conditional conformances behind an “experimental” flag. by DougGregor · Pull Request #13124 · apple/swift · GitHub)。
既に標準ライブラリ内では積極的に使われるようになっており、その有用性を示しています。例えば、Optional
、Array
、Dictionary
、Set
を条件付きでEncodable
やDecodable
に適合させる実装や、後述するKeyPath周りの改善、Array
の条件付きEqutable
適合などです。
概要
今まで「型引数が特定の条件のときのみ自身を特定のプロトコルに適合させる」ということはできませんでした。最小のコードを下に示します。
protocol P {} struct S<T> {} // 「Extension of type 'S' with constraints cannot have an inheritance clause」というエラーが出ます。 extension S: P where T: P {}
これが可能になるとどのような表現が可能になるのでしょうか。ここからは、プロポーザルに挙げられている例を元に説明します。
例:Array<Element>
問題
ところで、Swift 4.0ではArray<Element>
は==
で比較可能でしょうか。答えは「要素の型がEqutable
に適合しているかどうかによる」です。では、==
で比較可能なArray
はEqutable
でしょうか。答えは「NO」です。コードで示すと下記のような状態です。
// Equtableな要素を持つArray let intArray: Array<Int> = [1,2] // `==`で比較できます。 intArray == intArray // error: extension of type 'S' with constraints cannot have an inheritance clause checkEquatable(intArray) // `Array<Int>`は'Equatable'ではないので、以下の記述は不可です。 [intArray] == [intArray]
つまり、func ==<Element>(lhs: [Element], rhs: [Element]) -> Bool where Element : Equatable
によって==
が使用できる状態であり、Equtable
に適合しているわけではありません。Swiftはメソッドやプロパティで性質を判断するというより、プロトコルで一段階抽象化して性質を表現しますから、これはとても不自然です。理想的には「要素の型がEqutable
であればArray
自体もEqutable
」にしたいですよね。
// ## 理想 array == array // OKならば [array] == [array] // OK checkEquatable(element) // OKならば checkEquatable([element]) // OK
それを可能にするのが、今回の変更です。
「型引数が特定の条件のときは、その型自体が特定のプロトコルに適合する」 ↓ 「`Array`の持つ要素が`Equatable`であるときは、`Array`自体も`Equatable`に適合する」
コードにすると下記のように表現できます。
extension Array: Equatable where Element: Equatable { static func ==(lhs: Array<Element>, rhs: Array<Element>) -> Bool { ... } }
素晴らしいですね!ジェネリクス周りの進化方針についてはGenericsManifesto.mdに記載されており、今回の機能もその1つです。
associated typeへの再帰的なプロトコル制約
associated typeに再帰的な制約を付けられるようになりました(Swift 4.0では実装が間に合わなかったものが実装完了しました)。例えば、下記のようなコードです。
protocol P {f associatedtype _P: P }
P
のassociatedtype _P
の制約として自身(P
)を指定しています。以前までは「Type may not reference itself as a requirement」というエラーが出て使用不可でした。
ユースケース
Sequenceがあり、そのSequenceをスライスしたSub-Sequenceがあります。例えば、Array
に対してdrop
などをすると配列の一部を表すArraySlice
が取得できます。この場合、Sequenceに対応するのがArray
、Sub-Sequenceに対応するものがArraySlice
です。

Sequenceの一部(ArraySlice
)もSequence(Array
)の性質がありますよね。ここで、今回実装された機能が力を発揮します。
SequenceとSub-Sequenceの関係をコードで表すと、下記のようにassociatedtype
に再帰的にプロトコルの制約をつけることになります。
protocol Sequence { associatedtype SubSequence : Sequence ... }
この機能によって、標準ライブラリのコードが非常に綺麗になりました!(変更の詳細:SE-0157 Standard library uses of Recursive Protocol Constraints by DougGregor · Pull Request #11923 · apple/swift · GitHub)
その他の改善
Smart KeyPathsの改善
Swift 4.0で拡張されたSmart KeyPaths構文の提案「Smart KeyPaths: Better Key-Value Coding for Swift」の中でSwift 4.0時点では未実装の部分が完全に実装されました。
さらに、標準ライブラリの中のIndex Type
をHashable
に適合させる修正が入り、KeyPathでのsubscriptが使いやすくなりました。
// `String`や`Dictionary`などでも使えます。 let string = "swift" let firstChar = \String.[string.startIndex] string[keyPath: firstChar] // "s"
プロトコル内でのオーナーシップの宣言を削除
今までは、プロトコルのプロパティ宣言にweak
やunowned
を指定できました。実際には、それらに拘束力はなく、混乱を招くため、記述できないように変更されました。
protocol P { unowned var unownedVar: AnyObject { get } weak var weakVar: AnyObject? { get } } struct S: P { /* unowned */ var unownedVar: AnyObject /* weak */ var weakVar: AnyObject? }
Unsafe[Mutable][Raw][Buffer]PointerのAPI設計の改善
ポインタ周りの設計の見直しは、現在、大きなタスクとして動いています。11月中旬にマージされたSE-0184では、引数のラベルの変更、メソッドの追加などの修正が入りました。
最後に
Swift 4.1へのマイナーアップデートでもおもしろい変更が多く、リリースが待ち遠しいですね!ではでは、みなさま、良いお年を。
明日はstakayaさん、horikawaさん、oshiroさんによる「Japan.R 2017開催レポート」です。お楽しみに!
参考文献
- Swift CHANGELOG
- Synthesizing Equatable and Hashable conformance
- Synthesize Equatable/Hashable for complex enums, structs by allevato · Pull Request #9619 · apple/swift · GitHub
- GenericsManifesto.md
- Conditional conformances
- SE-0143 Put conditional conformances behind an “experimental” flag. by DougGregor · Pull Request #13124 · apple/swift · GitHub
- Smart KeyPaths: Better Key-Value Coding for Swift
- Remove ownership keyword support in protocols
- Unsafe[Mutable][Raw][Buffer]Pointer: add missing methods, adjust existing labels for clarity, and remove deallocation size
- Standard library uses of Recursive Protocol Constraints
- Support recursive constraints on associated types
- stdlib Make standard library index types Hashable by natecook1000 · Pull Request #12777 · apple/swift · GitHub