Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Swiftで直感的に書ける範囲比較演算を定義する

こんにちは、iOS開発チームの本柳です。

Pythonなどでは値の範囲チェック(m < x and n > xのような評価)をする時、数学の評価式のようなm < x < nと記述することが出来、大変分かりやすいですよね。

swiftでは、動的に変化する画面の座標が範囲内にあるか?という評価を行うケースがそれなりに多く、このような評価式をつくることが出来ると大変に便利。

便利ならば作ってしまおう!ということで~を使って似たような評価式を書けるようにしてみました。

オペレーターの定義

まずは、<,<=,>,>=に該当するオペレーターを定義します。

Pythonだとm > x > nのようにも書けますが、これはn < x < mと等価であるので、<,<=だけを書ければ良いはずです。

なので、<~!<=~に置き換えて定義出来るようにします。

// `<=`に該当するオペレーター
infix operator ~ { associativity left }
// `<`に該当するオペレーター
infix operator ~! { associativity left }

こうして定義することで、Swiftではm ~ x ~ nのように記述することが可能になります。 これはm <= x <= nと等価になります。

Swiftでは常に左辺の結果を利用したいので、オペレーターにはassociativity leftを指定して左結合するようにしておきます。

オペレーターが利用する処理の実装

実装方針は下記の通りです。

  • 最初の~オペレーターは中間比較オブジェクトを生成する
  • 次の~オペレーターは左辺に比較オブジェクトを取り、右辺にComparableな値をとって真偽値を返す

比較オブジェクト(_Rangeとして定義することにします)は中間状態、完全状態を持ちますが、この状態はファントムタイプで実装することにします。

まずはファントムタイプの定義です。

class _RangeState {}
class _RangeIntermediate: _RangeState {}
class _RangePerfection: _RangeState {}

次に_Rangeオブジェクトの定義を行います。

デフォルトの_Rangeオブジェクトはコンストラクタをプライベートにして、さらに、中間状態をもつインスタンスのファクトリメソッドを定義しておくことで、初回生成は常に中間状態を持つように定義します。

enum _Range_MinOp {
    case less
    case lessOrEq
}

enum _Range_MaxOp {
    case gt
    case gtOrEq
}

struct _Range<T: Comparable, U: _RangeState> {
    typealias MinOp = _Range_MinOp
    typealias MaxOp = _Range_MaxOp
    
    private let value: T
    private let min: T
    private let minOp: MinOp
    private let max: T?
    private let maxOp: MaxOp?

    // コンストラクタはプライベートにして、状態を持たないインスタンスを生成出来ないようにします
    private init(value: T, min: T, minOp: MinOp, max: T?, maxOp: MaxOp?) {
        self.value = value
        self.min = min
        self.minOp = minOp
        self.max = max
        self.maxOp = maxOp
    }

    // 初期状態では中間オブジェクトのみ生成することが出来ます
    static func buildIntermediate(value: T, min: T, minOp: MinOp) -> _Range<T, _RangeIntermediate> {
        return _Range<T, _RangeIntermediate>(value: value,
                                             min:   min,
                                             minOp: minOp,
                                             max:   nil,
                                             maxOp: nil)
    }
}

デフォルトの_Rangeオブジェクトの定義が出来たら、今度は中間状態の_Rangeオブジェクトを拡張して、完全状態の_Rangeオブジェクトを生成出来るようにします。

extension _Range where U: _RangeIntermediate {
    func toPerfection(max max: T, maxOp: MaxOp) -> _Range<T, _RangePerfection> {
        return _Range<T, _RangePerfection>(value: value,
                                           min:   min,
                                           minOp: minOp,
                                           max:   max,
                                           maxOp: maxOp)
    }
}

最後に完全状態の_Rangeインスタンスに値の評価する処理を実装すれば_Rangeオブジェクトは完成です。

extension _Range where U: _RangePerfection {
    var valueInRange: Bool {
        return valueLessThanMinimum && valueGtThanMaximum
    }

    private var valueLessThanMinimum: Bool {
        switch minOp {
        case .less:     return value > min
        case .lessOrEq: return value >= min
        }
    }

    private var valueGtThanMaximum: Bool {
        switch maxOp! {
        case .gt:     return value < max
        case .gtOrEq: return value <= max
        }
    }
}

オペレーターの関数を定義

ここまで出来たら最後に、演算子に適合させたい型とオペレーターを紐付ける処理を書いて完了です。

func ~<T: Comparable>(lhs: T, rhs: T) -> _Range<T, _RangeIntermediate> {
    return _Range<T, _RangeState>.buildIntermediate(rhs, min: lhs, minOp: .lessOrEq)
}

func ~!<T: Comparable>(lhs: T, rhs: T) -> _Range<T, _RangeIntermediate> {
    return _Range<T, _RangeState>.buildIntermediate(rhs, min: lhs, minOp: .less)
}

func ~<T: Comparable>(lhs: _Range<T, _RangeIntermediate>, rhs: T) -> Bool {
    let range = lhs.toPerfection(max: rhs, maxOp: .gtOrEq)
    return range.valueInRange
}

func ~!<T: Comparable>(lhs: _Range<T, _RangeIntermediate>, rhs: T) -> Bool {
    let range = lhs.toPerfection(max: rhs, maxOp: .gt)
    return range.valueInRange
}

これで下記のようにシンプルに範囲を比較する処理が書けるようになりました。

let value1 = 10
print(1 ~ value1 ~ 10)  // -> true
print(1 ~ value1 ~! 10) // -> false

let value2 = 1
print(1 ~ value2 ~ 10)   // -> true
print(1 ~! value2 ~ 10)  // -> false

株式会社ガラパゴスでは、Swiftをつかってプログラミングを楽しみたいエンジニアを絶賛大募集中です!

RECRUIT | 株式会社ガラパゴス iPhone/iPad/Androidのスマートフォンアプリ開発

たくさんのご応募、お待ちしております。