Swift 1.1 では独自の演算子を定義しない方が無難そう
Swift プログラミング
Swift では独自の演算子を定義できるようになっていますが、同じ記号の演算子を複数のモジュールで定義すると何かと困ったことになるようです。
同じ記号の演算子を複数のモジュールで定義すると途端に使えなくなったりするので、使う場所はかなり制限されそうです。
Swift言語
ではoperator
とfunc
を使って独自の演算子を定義できるようになっています。
prefix operator ∑ { }
prefix func ∑ (values:[Int]) -> Int {
return reduce(values, 0, +)
}
いっけんすると便利そうなのですけど、ここで気になるのは複数の場所で同じ演算子が定義された場合です。
たとえば、動作を実装するfunc
であれば扱う型を調整すれば区別もできそうですけど、独自の演算子を定義するときに使うoperator
には区別する手立てが思い浮かびません。
また、中置演算子 (infix operator
)にはoperator
で優先順位なども設定できるようになっているので、同じ記号で複数の演算子が定義されていたときに、その辺りがどうなるのかも気になります。
そんな訳で、いろんな場面でどんな動作を見せるかを Swift 1.1 で調べてみることにしました。
環境的には、ふたつのモジュール ModuleA, ModuleB を使ったアプリがあるとします。このとき、どのような状況でどのようなエラーになるかを記して行きます。
前提として、operator
で記載したもののことを、ここでは「定義」と呼ぶことにします。そしてその定義に沿って記載されたfunc
を「実装」と呼ぶことにします。なお、演算子はinfix
, prefix
, postfix
の3種類がありますが、同じ文字で定義した演算子でも種類が違う場合は、ここでは「異なる記号」として捉えることにします。なお、この中で語られている「演算子」は、特に断りがなければ「独自演算子」です。
ModuleA に演算子を定義&実装して、ModuleB には定義&実装しない場合
ModuleA だけで定義&実装した演算子をそのまま使うことは問題なし
まずはいちばん初歩的な場合を見てみましょう。
ModuleA で演算子を定義&実装した演算子を、ModuleA をインポートしたアプリ側で使用します。このとき、ModuleB には演算子の定義や実装はしません。
これは特に何も問題なく、とりあえずは実装どおりに動作します。
ModuleA
// 優先度が加減乗除より低い演算子「¡」をひとつ定義します。
infix operator ¡ {
precedence 135
associativity none
}
public func ¡ (lhs:Int, rhs:Int) -> Int {
return 0
}
// もうひとつ同じ内容の演算子「¡¡」を定義します。
infix operator ¡¡ {
precedence 135
associativity none
}
public func ¡¡ (lhs:Int, rhs:Int) -> Int {
return 0
}
ModuleB
// こちらには何も定義しません。
このようにすると、インポートしたアプリ側では次のように演算子を使えます。ここまでは普通な動きです。
let value1 = 10 ¡ 3
let value2 = 10 ¡¡ 3
ModuleA で実装した演算子と同じ記号の演算子をアプリで実装した場合
ここまでは何も問題ないように思えますが、アプリでも同じ演算子を定義したときに不思議なことが起こります。
たとえば、ModuleA で定義したのと同じ記号の演算子を、アプリ側にも異なる動作で実装していたとします。
アプリ
internal func ¡ (lhs:Int, rhs:Int) -> Int {
return 10
}
そうするとこの名前の演算子については、アプリ側に定義したものが優先的に使われます。
そのため、アプリ内でモジュール内で定義した演算子「¡」を使っていたところの動作は全て、アプリで定義した演算子の動作に置き変わります。
これは演算子に限らず関数やクラスなどでも同じなのですが、関数やクラスの場合は「モジュール名.」をプレフィックスにつけることで、モジュールに実装された方を選んで使うことができました。
ただし演算子の場合は、同じ感覚で let value1 = 10 ModuleA.¡ 3
などとしてみても次のようなエラーになります。
Operator with postfix spacing cannot start a subexpression
Expected member named following '.'
これは完全にただの構文エラーのようで、単純に名前空間を指定する方法がこれではないということが窺えます。
全体にカッコをつけたり、演算子名にカッコをつけたり、または普通の関数みたいに (ModuleA.¡)(10, 3) として使ってみても、構文エラーが解消することはありませんでした。
別の記載方法があるのか分からないですが、ともあれ演算子の場合はクラスや関数と違って名前が被りやすそうですし、引数と戻り値の型がうっかり一致したときに結果が全然違ってくることになっても気づきにくそうなところが心配です。
もちろん、演算子の実装を private
に閉じれば、影響範囲は実装したファイルの内側だけに限られます。
アプリ
internal func ¡ (lhs:Int, rhs:Int) -> Int {
return 10
}
ただ、そこまでしてわざわざ演算子を定義する必要もないでしょう。
ややこしいのでまとめると、次のような感じになります。
インポートしたモジュールに実装されている演算子と同名の演算子をアプリで… | 実装したファイル内で有効な演算子は… | それ以外のファイル内で有効な演算子は… |
---|---|---|
実装しない場合は | 実装していないので存在せず、実質的に ModuleA の実装が有効 | ModuleA をインポートした場合に、そこで実装されている演算子が有効 |
internal 指定で実装した場合は | アプリ側での新実装が有効 | ModuleA のインポート有無に関わらず、アプリ側での新実装が有効 |
private 指定で実装した場合は | アプリ側での新実装が有効 | ModuleA がインポートされていれば ModuleA での実装が有効、そうでなければ演算子の実装は存在しない |
そもそも論として「モジュールで定義した演算子をアプリ側では定義しない」というのもあるかもしれませんが、既に作り込んでいたアプリに新たにモジュールを加えたときに、アプリで新しく作って使っていた演算子が新しく使い始めたモジュールで使われていたりすると同じ問題が起こります。
ModuleA で定義した演算子をアプリ内で再定義した場合(実装はしない)
そして、演算子の再定義がまた違った影響を与えてきます。
たとえばアプリで、次のようにモジュールとは違った優先順位で演算子を再定義したとします。ちなみに ModuleA では優先順位は 135 に設定されています。
アプリ
infix operator ¡ {
precedence 150
associativity none
}
そうすると、この演算子「¡」の優先度が 150 になります。
このときとても重要なのが、アプリの演算子「¡」の優先度だけでなく、ModuleA で定義されている同じ記号の演算子「¡」の優先度も変わるところです。
つまり ModuleA で定義された演算子「¡」の優先度も 150 になります。
ただし影響は限定的で、ModuleA の優先度が変わるのは、再定義したファイルの内側だけに限られます。アプリの他のファイルでの優先度は ModuleA で定義されている優先度 135 が有効です。
まとめると、ModuleA で定義&実装されている演算子をアプリ側で再定義したときは…
再定義したファイル内では… | それ以外のファイルで ModuleA をインポートしたときは… | それ以外のファイルで ModuleA をインポートしないときは… |
---|---|---|
常にアプリ内での再定義が有効 | ModuleA での定義が有効 | (演算子の実装が存在しない) |
ModuleA で定義した演算子をアプリ内で再定義し、実装も行った場合
演算子の定義だけであれば、その演算子を使える場面はそれを定義したモジュールをインポートしたときに限られるので、影響範囲は把握できる程度の範囲で済みますけれど、ここにアプリ側での実装も加わると状況が大きく複雑化します。
たとえば、アプリで次のように演算子が実装されたとします。
アプリ
infix operator ¡ {
precedence 150
associativity none
}
internal func ¡ (lhs:Int, rhs:Int) -> Int {
return 10
}
アクセス範囲が internal
なので、この演算子の実装はアプリ内のすべてのファイルで有効になる訳ですが、この演算子の優先度は ModuleA をインポートするかどうかで変わってきます。
アプリ内で operator
を使って演算子を再定義すると、まず、再定義が記載されたファイル内では、ModuleA 内の同じ記号の演算子も含め、再定義どおりの優先度になります。
忘れてはいけないのは再定義をしたファイル以外の場合で、そこでは ModuleA がインポートされている場合に限り、演算子の優先順位が ModuleA で定義された 135 になります。そしてそのとき、演算子の動作は ModuleA 側の実装ではなく、アプリ側で実装した動作になります。
ModuleA がインポートされていない場合は、アプリ側で定義した優先度 150 が有効になります。演算子の動作は当然ですが、アプリ側で実装した動作です。
整理すると、ModuleA で定義したのと同盟の演算子を、アプリ内のあるファイルで再定義&実装を行ったときは…
以下に注目して見たときに… | 再定義&新実装したファイル内で有効なのは…(ModuleA のインポート有無を問わず) | 他ファイルで ModuleA をインポートした場合に有効なのは… | 他ファイルで ModuleA をインポートしなかった場合に有効なのは… |
---|---|---|---|
アプリ側の再定義 | アプリ側での再定義どおり | ModuleA の定義で上書き | アプリ側での再定義どおり |
アプリ側の新実装 (internal) | アプリ側での新実装どおり | アプリ側での新実装どおり | アプリ側での新実装どおり |
アプリ側の新実装 (private) | アプリ側での新実装どおり | 存在しないため、実質的に ModuleA の実装が使われる | (存在しない) |
ModuleA の定義 | アプリ側の再定義で上書き | ModuleA の定義どおり | (存在しない) |
ModuleA の実装 | 代わりにアプリ側の新実装が使われる | アプリ側の新実装が internal なら新実装が使われ、private なら ModuleA の実装が使われる | アプリ側の新実装が internal なら新実装が使われ、private なら存在しない |
このように、状況によって有効な組み合わせがいろいろ変わります。
すべてのモジュールを自分で作っているならなんとかなるかもしれませんが、さまざまな人が作ったモジュールを組み合わせて使うような場合には、演算子が意図したとおりに動くように整えるのは困難にも思えます。
ModuleA, ModuleB に、同じ定義で異なる実装の演算子を置いた場合
同じ定義で、異なる実装の演算子を作ったとき
次に ModuleA と ModuleB で、同じ記号で定義は同じ、実装の違う演算子を用意してみます。
ModuleA
infix operator ∆ {
precedence 135
associativity none
}
public func ∆ (lhs:Int, rhs:Int) -> Int {
return lhs + rhs
}
ModuleB
infix operator ∆ {
precedence 135
associativity none
}
public func ∆ (lhs:Int, rhs:Int) -> Int {
return lhs - rhs
}
このようにしたとき、次のように ModuleA と ModuleB とをインポートして次のように演算しようとするとエラーになります。
アプリ側
import ModuleA
import ModuleB
let v0 = 2 ∆ 3
Cannot invoke '∆' with an argument list of type '(IntegerLiteralConvertible, IntegerLiteralConvertible)'
これは、両辺にIntegerLiteralConvertible型 をとる演算子 '∆' を実行できないという意味になります。
それなら、演算子の定義に合わせてInt型 を明示的に渡してみるとどうなるでしょう。
import ModuleA
import ModuleB
let v0 = Int(2) ∆ Int(3)
Ambiguous use of operator '∆'
こうしたときに、演算子 '∆' が曖昧だというメッセージが表示されて、ここでようやく同じ記号の演算子が複数の場所で重複している可能性に気がつけます。
演算子を名前空間で区別する方法がわからないのは、前述の通りです。
片方のモジュールだけをインポートすれば利用可能だが…
両方のモジュールで同じ演算子が実装されているため、どちらのモジュールに実装されている演算子を使えばいいかが分からないのがエラーになる原因です。
つまり、インポートするモジュールを ModuleA か ModuleB かのどちらかにすれば、演算子を利用できるようになります。
ただし、もちろん ModuleA だけをインポートした場合は ModuleB に実装されている機能はすべて使えなくなります。
そして注意したいのが、ModuleA と ModuleB とで演算子の実装が異なっていれば、インポートするモジュールが ModuleA のときと ModuleB のときとで、演算結果が変わってくるところです。
引数や戻り値が異なれば使用可能
同じ記号の演算子でも、受け取る引数の型や戻り値の型によって区別できる場合は利用できます。
ModuleA
public func ∆ (lhs:Double, rhs:Double) -> Double {
return lhs + rhs
}
ModuleB
public func ∆ (lhs:String, rhs:String) -> String {
return lhs - rhs
}
たとえばこのように、ModuleA ではDouble型 を扱う演算子として実装され、ModuleB ではString型 を扱う演算子として実装されていたとすると、次のように普通に演算子を使うことができます。
アプリ側
let d = 10.5 ∆ 3.8
let s = "A" ∆ "B"
プロトコルを併用する方法も考えられるが…
つまり、どうしても独自演算子を使って処理をさせたいときには、他のモジュールで定義した同じ記号の演算子がぜったいに使わない型を扱うようにすれば実現できることになります。
それには自前でプロトコルを定義して、それに対して演算を許可するという方法が適切に思えるかもしれません。
たとえば ModuleA で次の定義があったとします。
ModuleA
public protocol DeltaOperatorComputable {
func + (lhs: Self, rhs: Self) -> Self
}
public func ∆ <T:DeltaOperatorComputable>(lhs:T, rhs:T) -> T {
return lhs + rhs
}
独自のDeltaOperatorComputableプロトコル を定義して、演算子ではそれを扱うものとして定義しています。
このようにした上で、たとえば ModuleA 内で、これに対応した構造体を、たとえば次のようにして定義しておいたとします。
ModuleA
public struct DeltaOperatorValue : DeltaOperatorComputable {
public private(set) var value:Int
public init(_ value:Int) {
self.value = value
}
}
public func + (lhs:DeltaOperatorValue, rhs:DeltaOperatorValue) -> DeltaOperatorValue {
return DeltaOperatorValue(lhs.value + rhs.value)
}
このとき、次のようなコードで普通にビルドが成功します。
アプリ側
let a = DeltaOperatorValue(2)
let b = DeltaOperatorValue(3)
let v = a ∆ b
こうであれば、もし ModuleB でまったく同じ定義がされていたとしても、値側の名前空間で対応できます。
アプリ側
let a = ModuleA.DeltaOperatorValue(2)
let b = ModuleA.DeltaOperatorValue(3)
let v = a ∆ b
そしてもしアプリ側で、Int型
をこの演算子に対応させたいと思ったときにはextension
で対応できます。
extension Int : ModuleA.DeltaOperatorComputable {
}
こうすることでInt型 でも普通に演算できるようになります。
let value = 2 ∆ 3
上の例では ModuleA の演算子が使われますが、extension
で ModuleB.DeltaOperatorComputable に対応させれば、演算は ModuleB のものが使われるようになります。
ただし、明示的にInt型 を扱う '∆' 演算子が他で定義されていたときには衝突して使えません。
そのときはプロトコルを明示する必要がありますが、今回のようにプロトコル自体がジェネリックになっていると単純にキャストできないところが厄介です。
そんなときにはジェネリック関数を使って対応できますが、これではほとんどただの再定義なので価値はなさそうです。
func operation<T:ModuleA.DeltaOperatorComputable>(lhs:T, rhs:T) -> T {
return lhs ∆ rhs
}
let value = operation(2, 3)
クロージャーでジェネリックが使えればもう少しは簡単になるのでしょうけど、残念ながら使えないのでこれで限界です。
さらには結局、演算子の優先度などは今まで紹介したように、アプリ側で上書きしてしまえば、その上書きしたファイルに限っては ModuleA と ModuleB の演算子の優先度が変わってしまいます。
そのため、演算子自体を名前空間で制御できたところで、優先順位が意図したものと変わってしまうことがあるのを気にしないといけないことには変わりません。
ふたつのモジュールで、同じ記号で異なる定義がされた演算子の優先度は?
ところで ModuleA と ModuleB とで同じ記号の演算子を、異なる優先度で定義して実装した場合はどうなるでしょう。
ModuleA
infix operator † {
precedence 135
associativity none
}
public func † (lhs:Int64, rhs:Int64) -> Int64 {
return lhs * rhs
}
ModuleB
infix operator † {
precedence 165
associativity none
}
public func † (lhs:UInt64, rhs:UInt64) -> UInt64 {
return lhs * rhs
}
このとき、次のようなコードを書くとビルドエラーが発生します。
アプリ側
let v1 = Int64(2) † Int64(3) + Int64(1)
let v2 = UInt64(2) † UInt64(3) + UInt64(1)
Ambiguous operator declarations found for operator
演算子の定義が曖昧だと指摘されています。
型によって実行される演算子が切り替わるのは先ほど試したとおりですが、今回のように同じ演算子の定義が両方にあってその内容(優先順位など)が異なる場合、どちらの演算子とも使えなくなります。
これを使えるようにするには、どちらか片方のモジュールだけをインポートして衝突を避けるしか手はなさそうです。
つまり、たかだか同じ演算子が定義されただけで「どちらかのモジュール全体を捨てる」必要性に迫られます。もしくは該当する演算子を使わなければエラーにならないので「両方のモジュールの」演算子を使わないという選択もあります。
いずれにしても、演算子を実装することで提供できるメリットよりも遥かに大きいデメリットをもたらしそうな予感がします。
ところでもうひとつ、両方の演算子を使えるようにする方法があります。
それには、アプリで両方のモジュールを定義するのと合わせて、演算子の再定義を行う方法です。
アプリ側
infix operator † {
precedence 255
associativity none
}
そうすると、これを記載したファイル内に限って、ModuleA と ModuleB の両方に実装された演算子を使えるようになります。ただし、もともとの演算子の定義は上書きされることになるので、この方法は根本的な対応策とは程遠いものです。
演算子を使いたいすべてのファイルで再定義が必要になることもあり、優先度の変更が必要になったときの再修正や、どこかのファイルで記載を間違えたときの原因の特定など、将来的な問題点も山積です。
標準の演算子も再定義できる
さて、これまで見てきた中で「演算子を再定義すれば…」という話を幾つかしてきましたが、標準の演算子の定義も同じように上書きできます。
infix operator + {
precedence 255
associativity none
}
こうすることで、これを定義したファイルに限り、掛け算よりも優先度の高い加算演算子の出来上がりです。これってさりげなく、とてつもなく怖いことをしているように感じます。
定義できる演算子が限られている
最後に小さなことですが、たとえば prefix !
は定義できますが、postfix !
は定義できません。また、ドット演算子よりも優先度の高い prefix 演算子は定義できない様子です。
演算子で使う記号も、たとえば「∑」のような記号や、「®©」のような記号を組み合わせたものは使えますが、「ß」みたいな文字的な記号は使えません。
もし将来的にシステムだけでしか使えない演算子が定められたりしたときに、もしそれと同じ演算子を自分で定義してたとすると、演算子を変更せざるを得ないといった状況になるかもしれません。
そうなると、そのモジュールを使ったコードの見た目的な意味も変わってきてしまうため、できるだけ避けたいところです。
Swift 1.1 で独自演算子を使うのは時期尚早?
以上から Swift 1.1 の独自演算子は、Swift にはめずらしく明らかな場当たり的にも感じられます。
どうにも正しい設計がされているようにも思えないので、もし独自演算子を定義しているモジュールを自作している場合は即刻削除しておいたほうが無難でしょう。
演算子を実装するときの心がけとしては、次のようになるでしょうか。
- 標準で定義された演算子で、独自の型を扱う演算子を作る。
- 独自演算子は使わない。
今のところは prefix 演算子と postfix 演算子であれば優先順位などの指定がないため、同じ記号であれば内容が変わることはないので、これなら型さえ独自のものを扱うように心がければ、使えないことはないかもしれません。
ただし、もし将来 prefix 演算子と postfix 演算子に何か属性がつけられるようになったときには、別の演算記号に改めるか、それこそ演算子を廃止したりしないといけなくなるくらいに影響範囲が大きいところが気になります。
これまで見てきたように、演算子自体の挙動が整っていない感でいっぱいなので、それに手を入れられて洗練されたと感じさせてくれるまでは手控えておくのが賢明そうです。