5
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の種類が足りない!① 〜演算子との戦い〜

Last updated at Posted at 2018-07-23

概要

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は"."のみなので。

5
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
5
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?