Swift 5.0 日本語化計画 : Swift 5.0
標準ライブラリの条件準拠
2018年1月8日   Ben Cohen
Swift 4.1 コンパイラは、汎用のロードマップ : 条件準拠 から次の段階の改善をもたらします。
この記事では、Swift の標準ライブラリでとても期待されていた機能がどのように採用されたのか、そしてそれがあなたとあなたのコードにどのような影響を与えるかを見ていきます。
Equatable のコンテナ
条件準拠の最も顕著な利点は、Array や Optional のような他の型を格納する型が Equatable プロトコルに準拠できることです。これは、型の 2 つのインスタンス間で == を使用できることを保証するプロトコルです。なぜこのプロトコルへの準拠がそんなに有用であるかを見てみましょう。
あなたは、equatable 要素の 2 つの配列で常に == を使うことができました:
[1,2,3] == [1,2,3] // true [1,2,3] == [4,5] // false
equatable の型を包み込む 2 つの optional では:
// The failable initializer from a String returns an Int? Int("1") == Int("1") // true Int("1") == Int("2") // false Int("1") == Int("swift") // false, Int("swift") is nil
これは、Array の場合のように、== 演算子のオーバーロードによって可能でした。
extension Array where Element: Equatable { public static func ==(lhs: [Element], rhs: [Element]) -> Bool { return lhs.elementsEqual(rhs) } }
しかし、== を実装したという理由だけで、Array または Optional が Equatable に準拠した事を意味しません。これらの型は equatable でない型を格納できるため、equatable 型を格納するときにのみ equatable であることを表現できる必要がありました。
つまり、これらの == 演算子には大きな制限がありました。2 つのレベルの深さで使用できませんでした。あなたが Swift 4.0 でこれを試したなら:
// convert a [String] to [Int?] let a = ["1","2","x"].map(Int.init) a == [1,2,nil] // expecting 'true'
コンパイラエラーが発生するでしょう:
Binary operator ‘==’ cannot be applied to two ‘[Int?]’ operands. (二項演算子 '=='は2つの '[Int?]'オペランドには適用できません。)
これは、上記のように Array に == を実装するには、配列の要素が equatable である必要があり、Optional が equatable でなかったためです。
条件準拠によって、これを修正できるようになりました。これは、すでに定義されている == 演算子を使用して、それらが基にしている型が equatable である場合、これらの型が Equatable に準拠していることを記述することができます。
extension Array: Equatable where Element: Equatable { // implementation of == for Array } extension Optional: Equatable where Wrapped: Equatable { // implementation of == for Optional }
Equatable 準拠は、== を超える他の利点をもたらします。equatable 要素を持つと、コレクションに検索のようなタスクのための他のヘルパー関数が与えられます:
a.contains(nil) // true [[1,2],[3,4],[]].index(of: []) // 2
条件準拠を使用すると、Swift 4.1 の Optional、Array、および Dictionary は、それらの値または要素がこれらのプロトコルに準拠するときは常に Equatable および Hashable に準拠します。
このアプローチは、Codable にも有効です。codable でない型の配列をコード化しようとすると、取得できていた実行時トラップの代わりにコンパイル時エラーが発生するようになります。
Collection のプロトコル
条件準拠はまた、コードの重複を避けて、あなたの型の能力を段階的にビルドすることにも利点があります。条件準拠を使用して Swift 標準ライブラリで可能になった変更のいくつかを調べるために、Collection:lazy splitting に新しい機能を追加する例を使用します。collection から分割 (split) されたスライスを提供する新しい型を作成すると、基本の collection が双方向である場合に双方向機能を追加するために条件準拠をどのように追加できるかを確認します。
熱望 (Eager) 対 Lazy 分割
Swift の Sequence プロトコルには split メソッドがあり、それはシーケンスを部分シーケンスの Array に分割します。
let numbers = "15,x,25,2" let splits = numbers.split(separator: ",") // splits == ["15","x","25","2"] var sum = 0 for i in splits { sum += Int(i) ?? 0 } // sum == 42
シーケンスを部分シーケンスに分割し、呼び出すとすぐに配列に配置するため、この分割メソッドは "熱心" (eager) であると特徴付けます。
しかし、ほんの最初のいくつかの部分シーケンスだけがほしいと思ったらどうですか?巨大なテキストファイルがあって、プレビューとして表示できる最初の行だけを取得したいとします。冒頭のいくつかの行を使用するだけで、ファイル全体を処理する必要はありません。
この種の問題はまた、map や filter のような操作にも適用され、これらは、Swift ではデフォルトで同様に eager です。それを避けるために、標準ライブラリには "lazy(遅延した)" シーケンスやコレクションがあります。あなたは lazy プロパティを介してそれらにアクセスできます。これらの遅延したシーケンスやコレクションでは、すぐに実行されない map のような操作の実装をしています。代わりに、要素にアクセスしたときこっそりそのマッピングやフィルタリングを実行するには例えば:
// a huge collection let giant = 0..<Int.max // lazily map it: no work is done yet let mapped = giant.lazy.map { $0 * 2 } // sum the first few elements let sum = mapped.prefix(10).reduce(0, +) // sum == 90
mapped コレクションが作成されても、マッピングは行われません。実際には、giant のすべての要素に対してマッピング操作を実行すると、トラップしてしまうことがある事に気付くでしょう。値を倍にすると Int に収まらない場合は、途中でオーバーフローしてしまいます。しかし、lazy map では、要素にアクセスするときにのみマッピングが行われます。したがって、この例では、最初の 10 個の値だけが計算され、reduce 演算によってそれらが合計されます。
遅延した分割ラッパー
標準ライブラリには、遅延分割操作はありません。以下は、どのように働くかのスケッチです。Swift に貢献することに興味があるなら、これは素晴らしい 初期のバグ と 改革の提案 になるでしょう。
まず、任意の基本コレクションを保持できる単純な汎用ラッパー構造体と、分割する要素を識別するクロージャを作成します。
struct LazySplitCollection<Base: Collection> { let base: Base let isSeparator: (Base.Element) -> Bool }
(私たちはこの記事のコードを単純にするためアクセス制御のようなものを無視しています)
次に、Collection プロトコルに準拠します。コレクションになるには、startIndex と endIndex、与えられたインデックスの要素を与える subscript、およびインデックスを 1 だけ進める index(after:) メソッドの 4 つのものだけを提供する必要があります。
このコレクションの要素は、基本となるコレクションの部分シーケンス ("one、two、three" からのサブストリング "one") です。コレクションの部分シーケンスは親コレクションと同じインデックス型を使用するため、基本となるコレクションのインデックスもインデックスとして再利用できます。インデックスは、基本内の次の部分シーケンスの開始点、または終了点になります。
extension LazySplitCollection: Collection { typealias Element = Base.SubSequence typealias Index = Base.Index var startIndex: Index { return base.startIndex } var endIndex: Index { return base.endIndex } subscript(i: Index) -> Element { let separator = base[i...].index(where: isSeparator) return base[i..<(separator ?? endIndex)] } func index(after i: Index) -> Index { let separator = base[i...].index(where: isSeparator) return separator.map(base.index(after:)) ?? endIndex } }
次のセパレータを見つけてその間のシーケンスを返す作業は、subscript と index(after:) メソッドとで行われます。どちらの場合も、次のセパレータのために、与えられたインデックスから基本となるコレクションを検索します。存在しない場合、index(where:) は見つからない場合 nil を返すので、その場合は ?? endIndex を使用して終了インデックスを置き換えます。やっかいな部分は、optional のマップ を使用した index(after:) 実装のセパレータをスキップしている所だけです。
lazy の拡張
このラッパー (wrapper) が完成したので、すべての遅延 (lazy) コレクション型を拡張して遅延分割メソッドで使用したいと思います。すべての遅延コレクションは LazyCollectionProtocol に準拠しているため、私たちのメソッドで拡張したのです:
extension LazyCollectionProtocol { func split( whereSeparator matches: @escaping (Element) -> Bool ) -> LazySplitCollection<Elements> { return LazySplitCollection(base: elements, isSeparator: matches) } }
また、要素が equatable のときにクロージャの代わりに値をとるバージョンを提供するために、このようなメソッドを使うのが慣例です:
extension LazyCollectionProtocol where Element: Equatable { func split(separator: Element) -> LazySplitCollection<Elements> { return LazySplitCollection(base: elements) { $0 == separator } } }
これで、lazy split メソッドを遅延サブシステムに追加しました。
let one = "one,two,three".lazy.split(separator: ",").first
// one == "one"
また、lazy ラッパーを LazyCollectionProtocol でマークして、それ以降の操作も遅延したものにしたいとも考えています。
extension LazySplitCollection: LazyCollectionProtocol { }
条件により双方向 (Bidirectioal)
これで、最初のいくつかの要素を、区切られたコレクションから効率的に分割できるようになりました。最後の few を読むのはどうですか?BidirectionalCollection は、index(before:) メソッドを追加して、インデックスを末尾から逆に移動します。これにより、双方向コレクションが last プロパティのようなものをサポートできるようになります。
私たちが分割しているコレクションが双方向であるならば、分割ラッパーも双方向にできるべきです。Swift 4.0 では、これを行う方法はかなりぎこちないものでした。Base: BidirectionalCollection と BidirectionalCollection の実装が必要な全く新しい型の LazySplitBidirectionalCollection を追加しなければなりません。次に、split メソッドをオーバーロードして where Base: BidirectionalCollection に戻します。
条件準拠では、はるかに単純な解決策があります。その基本が準拠したときに LazySplitCollection を BidirectionalCollection に準拠させるだけです。
extension LazySplitCollection: BidirectionalCollection where Base: BidirectionalCollection { func index(before i: Index) -> Index { let reversed = base[..<base.index(before: i)].reversed() let separator = reversed.index(where: isSeparator) return separator?.base ?? startIndex } }
ここで、どれでも双方向コレクションの順序を逆転させるもう 1 つの遅延ラッパー、reversed() を使用しました。これにより、次のセパレータを逆方向に検索し、逆コレクションインデックスの .base プロパティを使用して、元のコレクションのインデックスに戻ることができます。
この 1 つの新しいメソッドでは、.last プロパティや reversed() メソッドのような、全ての双方向コレクションに、遅延コレクションにアクセスできる機能を与えます。
let backwards = "one,two,three"
.lazy.split(separator: ",")
.reversed().joined(separator: ",")
// backwards == "three,two,one"
この種の増分条件準拠は、複数の異なる独立した準拠を組み合わせなければならないときには本当に輝きます。基礎となる物が変更可能なときはいつでも、私たちの遅延 Splitter(分割) を MutableCollection に準拠させたいとしましょう。これらの 2 つの準拠は、独立しており、変更可能なコレクションは双方向である必要はなく、その逆もありませんので、2 つの可能な組み合わせごとに特殊な型を作成する必要があります。
しかし、条件準拠では、2 番目の条件準拠を追加するだけです。
この機能は、標準ライブラリの slice 型が必要とするものとまったく同じです。この型は、任意のコレクション型にデフォルトのスライス機能を提供します。私たちの lazy (遅延した) splitter をスライスしようとすると、これを見ることがなります:
// dropFirst() creates a slice without the first element of a collection let slice = "a,b,c".lazy.split(separator: ",").dropFirst() print(type(of: slice)) // prints: Slice<LazySplitCollection<String>>
Swift 4 では、最悪の場合 MutableRangeReplaceableRandomAccessSlice まで、十個以上の異なる実装が必要でした。条件準拠では、4 つの異なる条件準拠を持つただ 1 つの slice 型だけになります。この変更だけで、標準ライブラリのバイナリサイズが 5% 減少しました。
さらなる実験
熱心な (eager) split にあなたが精通している場合は、実装で空の部分シーケンスを結合するなどのいくつかの機能が欠落していることがわかるでしょう。ラッパーに次のセパレータの場所をキャッシュする独自のカスタムインデックスを与えるなど、パフォーマンスの最適化を行うこともできます。
最初から独自の lazy ラッパーを作成したい場合は、一度に長さ n のスライスを提供する「チャンク」ラッパーも考えてください。この場合は、基本となるものがランダムアクセスの場合は BidirectionalCollection にすることができますが、基礎となるものが双方向の場合はできません。なぜなら、一定時間内に最後の要素の長さを計算できる必要があるからです。
現在、Swift 4.1 開発ツールチェーンでは条件準拠が利用できるため、最新のスナップショットをダウンロード して試用することができます。
<-Swift 4.1 リリースの過程 Swift フォーラム開店!->