uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Groovyの素敵(?)なインクリメント・デクリメント

Groovyのインクリメント・デクリメントの演算子++, --は、メソッドprevious(), next()にマップされていて、これらのメソッドを定義もしくはオーバーライド定義することで、演算子のオーバーロードを行うことができます(その他、plus,minus,..など、結構な種類の演算子を同様にオーバーロード可能です)。

で、疑問なのは、インクリメント・デクリメントの演算子に関して、前置・後置を区別して2つ定義しなくていいいのかということです。C++だと、前置用と後置用にそれぞれに定義が必要ですからね。

結論から言うと、不要です。あるべきnextを定義しておけば、前置と後置の使い分けは処理系のほうでやってくれます。つまり、

 ++a

 a=a.next()

に展開され、

 a++

 tmp = a, a=a.next(), tmp

に展開されます。previous()も同様です。
C#もそうなってますね。賢い賢い。

ちなみに、これも勉強会で出た話題ですが、plus()を定義すると(しなくても)、a+=bはa=a.plus(b)に勝手に展開されます*1。だから、+=(などの演算と代入を行う演算子)をあえて定義する必要はないし、できないようにもなっています。

参考:/src/main/org/codehaus/groovy/classgen/AsmClassGenerator.javaのevaluatePostfixMethod()とか evaluateBinaryExpressionWithAsignment()とか。

後は余談

ちょっとはまった話です。「値のコピー」がおきない限り、前置か後置かの区別は意味無いのですね。なぜかというと、もし参照だったら、「tmp = a, a= a.next(), tmp」とやったときのtmpとaは同じものをさしているわけだから、前置だろうと後置だろうと同じ値が得られてしまうからです。前置・後置に違いがあるnext/previousをプリミティブ型ではなくクラス型に対して定義する場合、クラスをイミュータブル扱いにしておく必要があります。つまり、nextやpreviousの返り値としてはthisではなく新しい値を持った別のオブジェクトを返す(newする)必要があります。以下サンプルコード:

class MyClass {
   int i;
   def next() { new MyClass(i:i+10) }
   def previous() { new MyClass(i:i-10) }
}

m = new MyClass(i:3);
n = (++m).i
println n
m = new MyClass(i:3);
n = (m++).i
println n

(結果は 13,3が表示される)

果たして素敵か?

++は意味的に「メソッド呼び出し」では無いのに注意。++は単独のメソッド呼び出しとしては決して実現できません。変数そのものの値を変えてしまえるからです。a.method()は、aの参照変数としての値(つまり示すオブジェクト)を決して変更し得無いのに対して、a++はnext()の返り値によってはaの値を元のaとは別オブジェクトに変更することができます。「演算子はメソッド呼び出し」と位置づけたRubyに++が無いのはこのせいかもしれません。+=も同じなんですけれども、Rubyでは(Groovyでも)+=は再定義可能なメソッドではなく、それは+の演算を決めると+=が定まるという性質があるからそれを利用したシンタックスシュガーとして実現されています。対象性の観点からは「+=に対する2項演算子+」に該当するような、「++に対する副作用無く1を足した結果を返す単項演算子」がもしあれば、++の存在がきれいに収まります。「副作用なしで1を加えた結果を得る単項演算子(++の副作用なしバージョン)」の自然で一般的な表記が提案できれば、Rubyで++が利用可能になる選択肢が生まれてくるかもしれませんね。
有用性とか利用頻度の話はまた別として。

*1:aが2回評価される、といってるわけではないです。必要に応じてテンポラリ変数が使われます。

広告を非表示にする