はじめに
自分が物心ついたときから、Swiftには、~= と表記する二項演算子がありました。しかしながら、この演算子に『本来,右辺と左辺が逆じゃ無いの?』とずっと疑問を抱いていました。この度、それが解決したので周知の事実とは思いますが、ここにまとめておきます。
~= 演算子とは?
この演算子は、次のように書きます。
var value: Int
: :
if 0 ..< 10 ~= value { 真 } else { 偽 }
if 10 ... 19 ~= value { 真 } else { 偽 }
つまり、ある値が期待する範囲に収まっているかどうかを調べる演算子で、次のコードと等価です。
if 0 <= value && value < 10 { 真 } else { 偽 } // ..<
if 10 <= value && value <= 19 { 真 } else { 偽 } // ...
~= はvalue(変数)を2度書く必要がなく、コードも読みやすくなると思います。ですが、右辺と左辺が逆ならもっとスッキリすると思いませんか? 次のような書き方です。
if value ~= 0 ..< 10 { 真 } else { 偽 }
for文だとこう書きますしね。1
for item in 0 ..< 10 { 処理 }
それなら独自に演算子を定義すれば・・・となるわけですが、そもそも、なんで最初からこうなってないのか調べてみました。
~= の本来の目的(?)
まずはSwiftの標準ライブラリのドキュメントにはこう説明されています。No overview available.
えっ!、ClosedRange
には何の説明もありません。しかしRange
の方には次の説明がありました。
重要なのは2つ目の説明です。
〜= 演算子は、パターンマッチングのcase文で内部的に使用されます。 case文の範囲と照合すると、この演算子はバックグラウンドで呼び出されます。 (Google翻訳)
これですね。「case文のパターンマッチングに使われる」と明記されています。しかし、まだ左辺右辺の謎は解明されません。そこで、Swift 言語仕様はどうなっているのか、これもAppleのドキュメントで確認しました。〜= 演算子に関する説明がここにあります。
ここで分かったことは、2つ目の説明の方で「〜= 演算子をオーバーロードして、カスタマイズできる」ことです。これは独自のクラスなどをこのパターンに適合させる場合の説明であって、まだ謎は解けません。次はズバリif文の文法を確認しました。ここにあります。
condition-list
のところです。以降はスクショを省略しますが、condition-list
→ condition
→ case-condition
→ case pattern
→ pattern
→ expression-pattern
と続きます。
前述のcase文のところに出てきたexpression-pattern
にたどり着きました。
えっ!、どういうこと? if文の条件式にcaseラベルが書けるってこと? どうやらそうらしいです(気にしたことがなかった)。ネットをググると(2015年とちょっと古い情報ですが)以下のコード例を見つけました。
上のコードをよく眺めると、冒頭で説明した**~=**の使い方と似たような構文です。
if 0 ..< 10 ~= value { 真 } else { 偽 } //冒頭の ~= の例
if case 0 ..< 10 = value { 真 } else { 偽 } //if case の例
要はcase =
が~=
に置き換わったということです。つまり、構文を似たようにする明確な意図があって、~=演算子の時に左辺と右辺を入れ替えたりしてはダメだったんです(多分)。これを知って『なるほど、それで範囲の方が左なのか』と納得しました。
前述の説明で~=をオーバーロードできるとあったので、ちょっと試してみました。
import Foundation
for value in [9, 10] {
if 0 ..< 10 ~= value { print("true") } else { print("false") }
if case 0 ..< 10 = value { print("true") } else { print("false") }
switch value {
case 0 ..< 10: print("case match")
default: print("default")
}
print()
}
extension Range {
static func ~= (left: Range<Bound>, right: Bound) -> Bool {
print("Range(\(left.lowerBound)-\(left.upperBound)) ~= \(right)")
return left.lowerBound <= right && right < left.upperBound
}
}
すごいです、本当にオーバロードした**~=**関数が呼ばれています。
Range(0-10) ~= 9
true
Range(0-10) ~= 9
true
Range(0-10) ~= 9
case match
Range(0-10) ~= 10
false
Range(0-10) ~= 10
false
Range(0-10) ~= 10
default
というわけで、
独自に =~ 二項演算子を定義する
~= 演算子がcase文のパターンマッチングの為にあるとするなら、自分が思う右辺に範囲がくる独自の =~ 演算子を定義することにしました。
定義は次のようになります。
infix operator =~ : ComparisonPrecedence
infix operator !=~ : ComparisonPrecedence
func =~ <T: FixedWidthInteger>(left: T, right: Range<T>) -> Bool { right ~= left }
func !=~ <T: FixedWidthInteger>(left: T, right: Range<T>) -> Bool { !(right ~= left) }
func =~ <T: FixedWidthInteger>(left: T, right: ClosedRange<T>) -> Bool { right ~= left }
func !=~ <T: FixedWidthInteger>(left: T, right: ClosedRange<T>) -> Bool { !(right ~= left) }
ついでに、範囲外を意味する否定形の演算子 !=~ も定義しました。
対象をFixedWidthInteger
として、範囲指定の2つの書き方..<
と...
でRange
とClosedRange
に分かれている 2 ため、それぞれ2組書きました。
シンプルに下記のように書きたいのですが、error: use of undeclared type 'Bound'
と怒られ1組で書く方法が分かりませんでした。
@MasasaM_shiさんに間違いを教えていただき、無事に1組みで定義できるようになりました。
func =~ <T: FixedWidthInteger, R: RangeExpression>(left: T, right: R) -> Bool where R.Bound == T { right ~= left }
func !=~ <T: FixedWidthInteger, R: RangeExpression>(left: T, right: R) -> Bool where R.Bound == T { !(right ~= left) }
終わりに
~= の謎は自分としてはスッキリ解決しました。いろいろ調べていくと先輩たちが情報を残してくれていて助かりました。ここにまとめたのも、これから同じ謎にぶち当たる方の助けになればとの思いです(もうそんな人は居ないかも・・・)。
本当はfor in
に倣ってin
(と!in
)にしたかったのですが、in
はSwiftでは演算子としては定義できない予約語でした。
間違いとか、もっと良い書き方があるとかあれば、ぜひ教えてください。
以上