4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-23

註: 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かどうかで、空かどうかの判定が変わってくることもあって、それをプロトコル側で実装したくなかったので…。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?