Edited at

[Swift] Rangeの種類が足りない!② 〜初めてのtype erasure〜


註: Ver. 3.0.0から仕様変更があって、この記事の内容は古くなっています。ただ、簡単なtype erasureの実装について参考になる(なればいいなー)と思いますので、記事はこのまま残しておきます。



概要

前回、Swiftに足りないRangeを実装したよ。

そこで、いろんなRangeを一貫性を持って扱えるようにAnyRangeというtype erasureを実装したよ!

初めてtype erasureを実装して勉強になったよ!


なぜAnyRangeが必要?

次のような「断続的な範囲」を表す構造体が欲しくて。そのためにはAnyRangeみたいなtype erasureがあると便利かなって…。


MultipleRanges.swift

public struct MultipleRanges<Bound> where Bound:Comparable {

private var _ranges: [AnyRange<Bound>] = []
// RangeExpressionはassociatedtypeを持っているため
// var _ranges:[RangeExpression] とはできない
}


却下案

public enum AnyRange<Bound> where Bound: Comparable {

case empty
case unbounded
case closedRange(ClosedRange<Bound>)
case leftOpenRange(LeftOpenRange<Bound>)
case openRange(OpenRange<Bound>)
case range(Range<Bound>)
case partialRangeFrom(PartialRangeFrom<Bound>)
case partialRangeGreaterThan(PartialRangeGreaterThan<Bound>)
case partialRangeThrough(PartialRangeThrough<Bound>)
case partialRangeUpTo(PartialRangeUpTo<Bound>)
}

所詮は範囲を表す構造体の種類なんて有限個だから、ということで力技で実装する案。

別にこれはこれでよかったのですが、次の2つの理由から却下となりました:


  • Swiftのバグ(SR-8192)があった(ただし、Swift 4.2では解決済み)。


  • overlaps(_:)などを実装しようとするときに煩雑になった。


採用案 〜自前のプロトコルを用意する〜


既存のプロトコルを知る

SwiftにはRangeExpressionというプロトコルがあります。しかし、このプロトコルは

func contains(_ element: Self.Bound) -> Bool

func relative<C>(to collection: C) -> Range<Self.Bound> where C : Collection, Self.Bound == C.Index

という2つのインスタンスメソッドぐらいしか定義されておらず、“これでtypeをeraseしちゃったら何も残らないじゃん”というわけで、このプロトコロルを元にtype erasureを作るわけにはいかないと結論づけました。


新たなプロトコルを作る


「範囲」とは

新たなプロトコルを作るにあたって、まず「範囲」とは何かを考えることにしました。

まず、範囲とは「“ここからここまで”というもの」だと思ったので、“ここから”と“ここまで”、即ち範囲の境界というものを表す物を作ることにしました。


範囲の境界を表す構造体


Boundary.swift

public struct Boundary<Bound> where Bound: Comparable {

public let bound: Bound
public let isIncluded: Bool
}

ということで、範囲の両端(のどちらか)を表す構造体を定義してみました。名前はBoundでもよかったのですが、RangeExpressionassociatedtypeであるBoundと被ってしまってややこしくなりそうだったのでBoundaryにしました。

範囲では、その境界自体を含むかどうかというのもあるので、let isIncluded: Boolというプロパティを用意しました。


「両端」を表すためには


GeneralizedRange.swift

public typealias Bounds<Bound> =

(lower:Boundary<Bound>?, upper:Boundary<Bound>?) where Bound:Comparable

構造体でも良かったのかもしれませんが、メソッドを持たせる予定もなかったのでtupleで済ませました。なお、Partial Rangeは「端っこがない」ということでnilで表します。たとえば、PartialRangeFromでは(lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:true), upper:nil)というような感じです。loweruppernilならunboundedと考えます。

このように、Bounds<Bound>を使えば任意の範囲を表すことができるはずです。


プロトコロルを定義する

これで下準備も整ったので、GeneralizedRangeというプロトコルを定義しました:


GeneralizedRange.swift

public protocol GeneralizedRange: RangeExpression {

var bounds: Bounds<Bound>? { get }
}

すごく単純で、前述のようにBounds<Bound>を使えば任意の範囲を表すことができるはずなので、プロパティとしてvar bounds: Bounds<Bound>? { get }さえ持っていればいいことにしました。なおOptionalになっているのは、空の範囲を表す場合はvar boundsの値としてnilを返すようにしたかったからです1


GeneralizedRangeを用いてtype erasureの実装


GeneralizedRangeに準拠させる

というわけで、いろんな範囲を表す構造体をGeneralizedRangeに準拠させてみましょう:

extension ClosedRange: GeneralizedRange {

public var bounds: Bounds<Bound>? {
if self.isEmpty { return nil }
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:true),
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:true))
}
}

extension LeftOpenRange: GeneralizedRange {
public var bounds:Bounds<Bound>? {
if self.isEmpty { return nil }
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:false),
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:true))
}
}

extension OpenRange: GeneralizedRange {
public var bounds:Bounds<Bound>? {
if self.isEmpty { return nil }
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:false),
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:false))
}
}

extension Range: GeneralizedRange {
public var bounds: Bounds<Bound>? {
if self.isEmpty { return nil }
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:true),
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:false))
}
}

extension PartialRangeFrom: GeneralizedRange {
public var bounds: Bounds<Bound>? {
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:true),
upper:nil)
}
}

extension PartialRangeGreaterThan: GeneralizedRange {
public var bounds: Bounds<Bound>? {
return (lower:Boundary<Bound>(bound:self.lowerBound, isIncluded:false),
upper:nil)
}
}

extension PartialRangeThrough: GeneralizedRange {
public var bounds: Bounds<Bound>? {
return (lower:nil,
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:true))
}
}

extension PartialRangeUpTo: GeneralizedRange {
public var bounds: Bounds<Bound>? {
return (lower:nil,
upper:Boundary<Bound>(bound:self.upperBound, isIncluded:false))
}
}


AnyRange<Bound>を実装

繰り返しになりますが、Bounds<Bound>?さえあれば任意の範囲を表すことができるので、AnyRange<Bound>はプロパティとしてBounds<Bound>?のみを保持することにします:


AnyRange.swift

public struct AnyRange<Bound> where Bound:Comparable {

private let _bounds: Bounds<Bound>?
}

あとは、引数にGeneralizedRangeに準拠するインスタンスを受け取るイニシャライザを実装すれば、type erasureの完成です!


AnyRange.swift

extension AnyRange {

public init<T>(_ range:T) where T:GeneralizedRange, T.Bound == Bound {
self._bounds = range.bounds
// 実際の実装では`bounds`の有効性チェックなどが入るので、
// もう少し複雑になっています。
}
}


おまけ: AnyRangeGeneralizedRangeに準拠させる

当然ながら、AnyRangeGeneralizedRangeに準拠させることは簡単です:


AnyRange.swift

extension AnyRange: GeneralizedRange {

public var bounds: Bounds<Bound>? {
return self._bounds
}
}


おわりに

Qiitaに「Swift の Type Erasure の実装パターンの紹介」という記事があったので、AnyRangeはどのパターンかなと探したのですが、どれでもありませんでした。もちろん、ここに載っていないぐらい複雑ということではなく、“ここに載っていないぐらい単純”ということです。

GeneralizedRangeにはメソッドがなくプロパティのみなので(というよりはAnyRangeで必要なのがプロパティのみなので)、クロージャもBoxも必要ないからです。

初めてのtype erasureの実装がAnyRangeで良かったです。Boxパターンが必要なものだったら挫折していたと思います。






  1. OpenRange<Bound>Boundがcountableかどうかで、空かどうかの判定が変わってくることもあって、それをプロトコル側で実装したくなかったので…。