Custom Operator in Swift3


Swift3におけるカスタムオペレータについて気になったので調べてみました。

構成

検証を行うため、以下の構成を準備しました。

OperatorFrameworkCocoa Touch Frameworkであり内部にOperator.swiftを持っています。Operator.swift内にてカスタムオペレータを定義しています。ApplicationiOS Applicationです。Applicationの内部でimport OperatorFrameworkをすることでカスタムオペレータを使用するという構成です。

検証

  1. 通常使用動作確認
  2. Application側でprecedencegroupを再定義する
  3. Application側でカスタムオペレータ実装を書き換える
  4. OperatorFramework側で使用しているオペレータをApplicationで再実装する
  5. 同じオペレータを実装する別のFrameworkをimportする

通常使用動作確認

Operator.swift内の実装は以下のようにします。

Operator.swift
precedencegroup Base {
    associativity: left
    lowerThan: AdditionPrecedence
}

infix operator <+> : Base
infix operator <*> : Base

public func <+> (lhs: Int, rhs: Int) -> Int {
    print("<+>: called `Base`")

    return lhs + rhs
}

public func <*> (lhs: Int, rhs: Int) -> Int {
    print("<*>: called `Base`")

    return lhs * rhs
}

左結合にし、AdditionPrecedenceよりも優先度を低くしています。<+><*>はそれぞれ2つのIntパラメータを取り、print logを吐き出しつつ和と積の結果を返します。

これをApplication側で使用します。

ViewController.swift

import UIKit
import OperatorFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(" ")
        print(" -------------------------------- ")
        print(2 <+> 3 <*> 4)
        print(" -------------------------------- ")
        print(" ")

    }
}

ログはこんな感じに。

左結合なので<+>である和が先に計算され、その後<*>である積が計算されます(20 = (2+3)*4)。

Application側でprecedencegroupを再定義する

では次にApplication側でprecedencegroupを再定義してみます。具体的には以下の通りに修正します。

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(" ")
        print(" -------------------------------- ")
        print(2 <+> 3 <*> 4)
        print(" -------------------------------- ")
        print(" ")

    }
}

/// 以下を追加

precedencegroup Hacked {
    associativity: left
    higherThan: MultiplicationPrecedence
}

infix operator <*> : Hacked

すると出力がこう変わります。

左結合であるのは変わらないのですが、優先度がHacked>Baseであるため14=2+(3*4)の順番に処理されます。つまり、framework側で複数のオペレータを実装している場合、 意図しない順序で実行される可能性がある ということです。

ちなみに、今回はHackedという名前にしましたが、Baseという同じ名前にしても問題なくビルドが通り、同様の挙動になります。prefix等は気にしないで良さそうです。

Application側でカスタムオペレータ実装を書き換える

次に実装まで記述します。

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(" ")
        print(" -------------------------------- ")
        print(2 <+> 3 <*> 4)
        print(" -------------------------------- ")
        print(" ")

    }
}

precedencegroup Hacked {
    associativity: left
    higherThan: MultiplicationPrecedence
}

infix operator <*> : Hacked

/// 以下を追加

func <*> (lhs: Int, rhs: Int) -> Int {
    print("<*>: called `Hacked`")

    return lhs
}

すると出力はこうなります。

Application側で実装された<*>が呼ばれ、5=2+3と成っています。では、<*>OperatorFramework側で使用していた場合はどうなるのでしょうか。

OperatorFramework側で使用しているオペレータをApplicationで再実装する

Application側はそのままにOperatorFramework側を以下のように書き換えます。

Operator.swift
precedencegroup Base {
    associativity: left
    lowerThan: AdditionPrecedence
}

infix operator <+> : Base
infix operator <*> : Base

public func <+> (lhs: Int, rhs: Int) -> Int {
    print("<+>: called `Base`")

    //return lhs + rhs
    return lhs <*> rhs  // <- New
}

public func <*> (lhs: Int, rhs: Int) -> Int {
    print("<*>: called `Base`")

    return lhs * rhs
}

すると出力は以下のようになります。

OperatorFramework.<+>内で使用している<*>OperatorFrameworkのものを呼び出しています。

同じオペレータを実装する別のFrameworkをimportする

新たにFramework(ここではOtherFrameworkとする)を定義し、その中で<+><*>を実装します。その状態でApplication側でimport OtherFrameworkするとビルドNGとなります。ただし、Application側で再実装するとビルドOKとなります。

ViewController.swift
import UIKit
import OperatorFramework
import OtherFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(" ")
        print(" -------------------------------- ")
        print(2 <+> 3 <*> 4)
        print(" -------------------------------- ")
        print(" ")

    }
}

precedencegroup Hacked {
    associativity: left
    higherThan: MultiplicationPrecedence
}

infix operator <*> : Hacked
infix operator <+> : Hacked

// 以下がないと`OperatorFramework`と`OtherFramework`の実装が衝突してビルドNG

func <*> (lhs: Int, rhs: Int) -> Int {
    print("<*>: called `Hacked`")

    return lhs
}

func <+> (lhs: Int, rhs: Int) -> Int {
    print("<+>: called `Hacked`")

    return lhs
}

まとめ

  • importする側で同じ演算子を再定義すると優先度が変化する
  • 同じオペレータを定義している2つのframworkをimportしようとするとビルドNG

部分的にimportするか部分的にimportしない仕組みがないうちは、カスタムオペレータをframwork側が提供するのは避けた方がいいのかもしれません。