Swift

[Swift] Rangeの種類が足りない!① 〜演算子との戦い〜

概要

Swiftにおいて標準で実装されていない“範囲を表す構造体”を実装してみたよ!
SwiftRangesという名前で公開したよ!
演算子と戦ったよ!

おさらい

Swiftには標準で“範囲を表す構造体”が5種類あります(Countable*は除く1; また、UnboundedRange別の記事で説明した通り構造体ではありません)。
範囲を表す構造体は次の通り:

  • ClosedRange: lower...upper
  • Range: lower..<upper
  • PartialRangeFrom: lower...
  • PartialRangeThrough: ...upper
  • PartialRangeUpTo: ..<upper

何が足りない?
…そう、「lowerを含まない範囲」というのが実装されていないのです。必要ないからかな?

足りないものは補おう

というわけで、「lowerを含まない範囲」を実装していきましょう。

  • LeftOpenRange: lowerは含まないけどupperは含む範囲
  • OpenRange: lowerもupperも含まない範囲
  • PartialRangeGreaterThan: lowerを含まず、上限がない範囲

これら3つの構造体を定義するのはとても簡単です。次のようなコードとなりました:

public struct LeftOpenRange<Bound: Comparable> {
  public let lowerBound: Bound
  public let upperBound: Bound
}

public struct OpenRange<Bound: Comparable> {
  public let lowerBound: Bound
  public let upperBound: Bound
  internal let _isEmpty: Bool
}

public struct PartialRangeGreaterThan<Bound: Comparable> {
  public let lowerBound: Bound
}

OpenRangeだけ_isEmptyとかいうプロパティがあるけど、この記事では気にしない気にしない。

演算子を定義しよう 〜実は一筋縄ではいかない〜

たとえば、0以上10未満を表すRange<Int>は、..<というinfix operatorを用いて0..<10というように書くことができます。となると、たとえば0超10以下を表すLeftOpenRange<Int>0<..10と書きたくなりませんか?私はなります。
では、さっそく、<..というinfix operatorを定義しましょう。

infix operator <.. :RangeFormationPrecedence

と書けば…

error: r.swift:1:17: error: consecutive statements on a line must be separated by ';'
infix operator <.. :RangeFormationPrecedence
                ^
                ;

error: r.swift:1:17: error: operator with postfix spacing cannot start a subexpression
infix operator <.. :RangeFormationPrecedence
                ^

error: r.swift:1:20: error: expected expression
infix operator <.. :RangeFormationPrecedence

…できません!

なぜ、できないのか?

定義できる演算子とは?

SwiftのLanguage Referenceを見てみましょう。Grammar of operatorsを引用します:

operator → operator-head operator-characters(opt)
operator → dot-operator-head dot-operator-characters
operator-head → / | = | - | + | ! | * | % | < | > | & | | | ^ | ~ | ?
operator-head → U+00A1–U+00A7
operator-head → U+00A9 or U+00AB
operator-head → U+00AC or U+00AE
operator-head → U+00B0–U+00B1, U+00B6, U+00BB, U+00BF, U+00D7, or U+00F7
operator-head → U+2016–U+2017 or U+2020–U+2027
operator-head → U+2030–U+203E
operator-head → U+2041–U+2053
operator-head → U+2055–U+205E
operator-head → U+2190–U+23FF
operator-head → U+2500–U+2775
operator-head → U+2794–U+2BFF
operator-head → U+2E00–U+2E7F
operator-head → U+3001–U+3003
operator-head → U+3008–U+3030
operator-character → operator-head
operator-character → U+0300–U+036F
operator-character → U+1DC0–U+1DFF
operator-character → U+20D0–U+20FF
operator-character → U+FE00–U+FE0F
operator-character → U+FE20–U+FE2F
operator-character → U+E0100–U+E01EF
operator-characters → operator-character operator-characters(opt)
dot-operator-head → .
dot-operator-character → . | operator-character
dot-operator-characters → dot-operator-character dot-operator-characters(opt)
binary-operator → operator
prefix-operator → operator
postfix-operator → operator

まず、定義できるoperatorは2種類あって、"operator-head operator-characters(opt)"と"dot-operator-head dot-operator-characters"です。よく見ると、operator-character(s).は含まれておらず、dot-operator-headdot-operator-chracter(s)にのみ.は含まれています。
簡単に言うと、.で始まる演算子以外では2文字目以降に.を使えない」のです2
結局、<..と書くと、<..という2つの演算子が並んでいると解釈されてしまいます。だから、上記のようなエラーになってしまったのです。

2つの演算子を定義すればいいんだ

Swiftでは0<..10と書くと(0<)..(10)というように解釈され得ることが分かりました。ということは、<というpostfix operatorと..というinfix operatorを定義すればうまくいくかもしれません。

まずはExcludedLowerBoundという構造体を使って前者を実装することにします:

public struct ExcludedLowerBound<Bound> where Bound: Comparable {
  public let lowerBound:Bound
  public init(_ lowerBound:Bound) { self.lowerBound = lowerBound }
}

postfix operator <
public postfix func < <T>(_ lowerBound:T) -> ExcludedLowerBound<T> where T: Comparable {
  return ExcludedLowerBound<T>(lowerBound)
}

こうすることで、0<と書くとExcludedLowerBound<Int>(0)と書くのと同じことになります。
次は、..の方の実装です:

extension LeftOpenRange {
  public init(uncheckedBounds bounds: (lower: Bound, upper: Bound)) {
    self.lowerBound = bounds.lower
    self.upperBound = bounds.upper
  }
}

infix operator ..: RangeFormationPrecedence
public func .. <T>(lhs:ExcludedLowerBound<T>, upper:T) -> LeftOpenRange<T> {
  let lower = lhs.lowerBound
  guard lower <= upper else {
    fatalError("Can't form Range with upperBound < lowerBound")
  }
  return LeftOpenRange(uncheckedBounds:(lower:lower, upper:upper))
}

左辺にExcludedLowerBound<T>型、右辺にT型をとる、infix operatorとして定義しました。
これで、0<..10(0<)..(10)ExcludedLowerBound<Int>(0) .. (10)LeftOpenRange<Int>(uncheckedBounds:(lower:0, upper:10))と同じことになりました!

同じように、OpenRangePartialRangeGreaterThanも実装することができます。

ここで定義した演算子の問題点

前述のように<はあくまでpostfix operatorなので、0 <.. 10のように0<のあいだにスペースを含めることができません(一方で0< .. 10はOK)。Swiftの言語仕様上、これが限界です。

もうちっとだけ続くんじゃ?

これだけ“範囲を表す構造体”が増えると、AnyRange<Bound>みたいなtype erasureを実装したくなりますよね。実際にSwiftRangesでは実装してあります。が、その話はまた別の機会にでも…。

→ 続き: 「Rangeの種類が足りない!② 〜初めてのtype erasure〜



  1. だってCountable*はSwift 4.2からConditional Conformance的なtypealiasになるからね→PR#13342 

  2. dot-operator-headは"."のみなので。