19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift] ~= 演算子についての考察

Last updated at Posted at 2020-04-18

はじめに

自分が物心ついたときから、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の方には次の説明がありました。

スクリーンショット 2020-04-18 7.57.37.png

重要なのは2つ目の説明です。

〜= 演算子は、パターンマッチングのcase文で内部的に使用されます。 case文の範囲と照合すると、この演算子はバックグラウンドで呼び出されます。 (Google翻訳)

これですね。「case文のパターンマッチングに使われる」と明記されています。しかし、まだ左辺右辺のは解明されません。そこで、Swift 言語仕様はどうなっているのか、これもAppleのドキュメントで確認しました。〜= 演算子に関する説明がここにあります。

スクリーンショット 2020-04-18 8.11.30.png

ここで分かったことは、2つ目の説明の方で「〜= 演算子をオーバーロードして、カスタマイズできる」ことです。これは独自のクラスなどをこのパターンに適合させる場合の説明であって、まだは解けません。次はズバリif文の文法を確認しました。ここにあります。
スクリーンショット 2020-04-18 9.13.43.png
condition-listのところです。以降はスクショを省略しますが、condition-listcondition → case-condition → case patternpattern → expression-patternと続きます。
前述のcase文のところに出てきたexpression-patternにたどり着きました。
えっ!、どういうこと? if文の条件式にcaseラベルが書けるってこと? どうやらそうらしいです(気にしたことがなかった)。ネットをググると(2015年とちょっと古い情報ですが)以下のコード例を見つけました。
スクリーンショット 2020-04-18 9.59.45.png

上のコードをよく眺めると、冒頭で説明した**~=**の使い方と似たような構文です。

     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つの書き方..<...RangeClosedRangeに分かれている 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では演算子としては定義できない予約語でした。
間違いとか、もっと良い書き方があるとかあれば、ぜひ教えてください。

以上

  1. for inはなぜ右側に範囲がくるのか、この謎も言語仕様と標準ライブラリ仕様を読むと理解できます。for inの右にくるのはcollectionSequenceプロトコルに適合する必要があります。RangeClosedRangeSequenceプロトコルに適合しているからです。

  2. 範囲の指定の仕方(記号)は、..<...2つですが、両端に指定する数値の書き方にバリエーションがあって、実際はもっと複雑です。これは@koherさんのこちらの記事に解説があります。(Swift4.1)

19
7
4

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
19
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?