Edited at

Custom Operator in Swift3

More than 1 year has passed since last update.

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


構成

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

160917-0002.png

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(" ")

}
}


ログはこんな感じに。

160917-sysoutImage_01.png

左結合なので<+>である和が先に計算され、その後<*>である積が計算されます(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


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

160917-sysoutImage_02.png

左結合であるのは変わらないのですが、優先度が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
}


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

160917-sysoutImage_03.png

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
}


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

160917-sysoutImage_04.png

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側が提供するのは避けた方がいいのかもしれません。