Swift 4.1+

こんにちは、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)へのEquatableHashable適合のための実装(==/hashValue)が、後述する条件を満たすことで暗黙的に実装される」という変更です。これ以降の節では、暗黙実装の条件や背景などの詳細を説明していきます。

EqutableHashableについて

最初に、EqutableHashableのおさらいです(不要な方は次の節からお読みください)。

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まで

標準ライブラリで提供されている型の多くはEquatableHashableに適合していますが、自作の型の場合、==hashValueの実装を自前で書く必要がありました(ただし、associated valueを持たないenumは暗黙的にEquatableHashableに適合しています)。

例えば、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) -> Boolvar hashValue: Int { get }が暗黙的に実装されます。

各プロトコルに必要な実装が暗黙的に実装されるには、関連している型(stored instance property、associated value)もそれぞれのプロトコルに適合している必要があります。

どちらの場合も、カスタマイズする必要があるなら、明示的に実装すればその実装が採用されます。

Equatableの場合

  1. 型の宣言部にEquatableの適合を宣言(注:extension部での宣言には適用されない)
  2. 下記の型もEquatableに適合
    • union style enumの場合:caseが保持する全てのassociated value
    • structの場合:全てのstored instance property
  3. 下記の条件を満たさない
    • union style enumの場合:caseがない(そもそもインスタンス化できない)

Hashableの場合

  1. 型の宣言部にHashableの適合を宣言(注:extension部での宣言には適用されない)
  2. 下記の型もHashableに適合
    • union style enumの場合:caseが保持する全てのassociated value
    • structの場合:全てのstored instance property
  3. 下記の条件を満たさない
    • 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)。

既に標準ライブラリ内では積極的に使われるようになっており、その有用性を示しています。例えば、OptionalArrayDictionarySetを条件付きでEncodableDecodableに適合させる実装や、後述する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に適合しているかどうかによる」です。では、==で比較可能なArrayEqutableでしょうか。答えは「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
}

Passociatedtype _Pの制約として自身(P)を指定しています。以前までは「Type may not reference itself as a requirement」というエラーが出て使用不可でした。

ユースケース

Sequenceがあり、そのSequenceをスライスしたSub-Sequenceがあります。例えば、Arrayに対してdropなどをすると配列の一部を表すArraySliceが取得できます。この場合、Sequenceに対応するのがArray、Sub-Sequenceに対応するものがArraySliceです。

sequenceとsub-sequence

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 TypeHashableに適合させる修正が入り、KeyPathでのsubscriptが使いやすくなりました。

// `String`や`Dictionary`などでも使えます。
let string = "swift"
let firstChar = \String.[string.startIndex]
string[keyPath: firstChar] // "s"

プロトコル内でのオーナーシップの宣言を削除

今までは、プロトコルのプロパティ宣言にweakunownedを指定できました。実際には、それらに拘束力はなく、混乱を招くため、記述できないように変更されました。

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開催レポート」です。お楽しみに!

参考文献

Related Post