Swift3におけるカスタムオペレータについて気になったので調べてみました。
構成
検証を行うため、以下の構成を準備しました。
OperatorFramework
はCocoa Touch Framework
であり内部にOperator.swift
を持っています。Operator.swift
内にてカスタムオペレータを定義しています。Application
はiOS Application
です。Application
の内部でimport OperatorFramework
をすることでカスタムオペレータを使用するという構成です。
検証
- 通常使用動作確認
-
Application
側でprecedencegroup
を再定義する -
Application
側でカスタムオペレータ実装を書き換える -
OperatorFramework
側で使用しているオペレータをApplication
で再実装する - 同じオペレータを実装する別のFrameworkをimportする
通常使用動作確認
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
側で使用します。
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
を再定義してみます。具体的には以下の通りに修正します。
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
側でカスタムオペレータ実装を書き換える
次に実装まで記述します。
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
側を以下のように書き換えます。
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となります。
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側が提供するのは避けた方がいいのかもしれません。