Hatena::ブログ(Diary)

scalaとか・・・ このページをアンテナに追加 RSSフィード Twitter

2011-01-30

scalaでは、なぜインクリメントやデクリメントができないのか?

| 13:05 | scalaでは、なぜインクリメントやデクリメントができないのか?を含むブックマーク scalaでは、なぜインクリメントやデクリメントができないのか?のブックマークコメント

javaでは、以下のように

int a = 0;
int b = ++a;
System.out.print(a); // 1 が表示される
System.out.print(b); // bも1

++というインクリメントするための演算子があります。

しかしscalaの場合

var a = 0
// a ++  これはダメ
val b = a += 1
print(a) // 1 が表示される
print(b) // b はUnit

というようにしないといけません。

この違いについてのおなはし(・ω・)ノ

これをちゃんと解説しようとすると、かなりややこしいんですが、それなりの理由があります。

すごく細かいところだけど、Javaとの違いで初心者が戸惑いやすいところだと思います。

Javaの言語仕様書の15.14 〜 15.18あたりだとおもいますが、++、+=、--、-=、などは、すべてOperator(演算子)として説明されてますよね?

しかし、Scalaには演算子は存在せず、すべてがメソッドです。scalaでは、

1 + 2

というのは、

1 .+(2)

とも書けます。

1というオブジェクトの、+というメソッドを呼び出し、その引数として2を渡す

という意味です。

なぜインクリメントの++というものが存在しないかというと、

  • javaでのprimitive型も、scalaでは普通のオブジェクトと統一的に扱う
  • つまり、演算子が存在せず、(IntやCharなどJavaではprimitive型と呼ばれているものも)すべてがオブジェクトであり、すべてがメソッド

という原則を優先したためです。

つまり、iがInt型のとき、++iや、i++という構文を許してしまうと、これらをメソッドを呼び出すという構文で解釈できないからです。

javaの場合のインクリメント

int i = 0;
int b = ++i;

が意味するところは、

  • i という変数の値を1増やす
  • かつ、 ++i という式全体としてもi+1した値を返す

という感じですかね。

しかし、scala的な考え方をすると、

  • var i = 0 というのは、"iという変数に0というIntのオブジェクトをbind(束縛)した" ということになる
  • Intというのは、immutableな(変更不可能で状態を持たない)オブジェクトである
  • よって、iという変数に再代入しない限り、iに束縛された0のメソッドを呼んでも、iは0のままであるはず

ということになります。

ここでいいたいのは、

インクリメントって、一度束縛された変数の値を、代入以外の方法で違うものに書き換えてるよね?しかもprimitive型しかそういう操作できないし。それってなんか統一的じゃなくね?

ってことです。整理すると以下のようになります

Java インクリメントやデクリメントなどの操作を行うことで、変数に違うオブジェクトを束縛しなおすことができる
scala 代入以外の方法では、変数への束縛ができない

ではなぜscalaで

var i = 0
i += 1

とした場合に、i (という変数に束縛される値)が1になるのか?というと、以下のような操作

var i = 0
i = i + 1

シンタックスシュガーだからです!

これがシンタックスシュガーというのは、コップ本*1323、324ページあたりに解説されています。以下引用

+= メソッドをサポートしていないaを使って a += bというコードを書くと、Scalaはa = a + bと解釈しようとしてみるのだ。

+=メソッドだけでなく、=で終わるあらゆるメソッドに同じ考えが適用される。

ところで、このシンタックスシュガーはコレクションだけでなく、あらゆるタイプの値で機能する。

このような効果は、Javaの+=,-=,*=などの代入演算子と似ているが、Scalaでは=で終わるすべての演算子で可能であり、この効果はより一般的なものとなる。

コップ本にもあるように、scalaでは、メソッド名が=で終わるものは特別扱いされます。それがどういうところで使うと便利なのかっていう詳細は、もう説明つかれたのでコップ本嫁ってことで・・・(´・ω・`)

たとえば、このルールがすべてのモノに適用されるので、(説明のための例で、実用的にはあまり意味がないですが)以下のようなことができます↓*2

scala> case class Test(val a:String){         
     | 
     |   def ++++(b:String) = Test(b)
     | 
     | }
defined class Test

scala> val x = Test("hoge")
x: Test = Test(hoge)

scala> x ++++= "fuga" // xはvalなので再代入できない
<console>:9: error: reassignment to val
       x ++++= "fuga"
         ^
scala> var y = Test("hoge")
y: Test = Test(hoge)

scala> y ++++= "fuga" // y = y.++++("fuga") のシンタックスシュガー

scala> y
res1: Test = Test(fuga) // yが Test(fuga) に置き換わってる

このscalaの考え方がすごくわかりやすいか?っていうとどうなんだろう、ってところはありますが・・・コップ本にあるように、collectionのimmutableなものと、mutableなものの切り替えなんかで有用だったりします。

*1:日本語第1版

*2:最初に載せたコード例が、若干不適切なので変更しました。Testクラスを、immutableにして、++++メソッドは、新しいインスタンスを返すように変更

@SigSasaki@SigSasaki 2011/01/30 15:15 Scala にインクリメント演算子、デクリメント演算子が無いのは、Ruby と同じ言語設計上の判断をしたからですよね。つまり (1)メソッド呼び出しは式を評価してから実行されるので、たとえば var n = 1; n++ は 1++ と解釈される。これはナンセンス。では (2) ++, -- をそれぞれ代入 +=1, -=1 のシンタックス・シュガーにすればどうかというと、今度は演算子オーバーロードに関する規則が複雑になる。よって不採用とした。

xuweixuwei 2011/01/30 15:38 そうですね、Rubyも同じですね。補足ありがとうございます

NewtGeckoNewtGecko 2014/02/09 16:31 ひしだまさんのリンク(http://www.ne.jp/asahi/hishidama/home/tech/scala/expression.html)からやって来ました。
Scalaにインクリメントがないのはなぜなのか?ということを疑問に思っていたので、記事を読んで、「Scala設計者が1や314のような値もオブジェクトであり、それを操作する演算子もメソッドとして捉えることができる」ということが理解できたと思います。ありがとうございます。

Javaは経験があったので、Scalaを最近始めて、演算子までもメソッドと捉えるScalaの文法に驚きました!
しかし、個人的にはインクリメントは便利なので、Scalaにも欲しかったです。


そこで、ScalaでインクリメントができるIntクラスを作ってみました。

class MyInt(var n: Int){
def ++ = {n +=1; new MyInt(n-1)}
def +=(that: Int) = n += that;
override def toString = n.toString;
}

// 動作確認
object Main {
def main(args: Array[String]){

var integer = new MyInt(123);
println(integer + "\t(初期値)");

integer++;
println(integer + "\t(インクリメント後)");

println((integer++) + "\t(println()内でインクリメント)");

integer += 10000;
println(integer + "\t(+=10000後)");
}
}

出力結果は
123 (初期値)
124 (インクリメント後)
124 (println()内でインクリメント)
10125 (+=10000後)
となりました。

+=メソッドを作ったのは、
n += 1

n = n + 1
のシンタックシュガーと捉えなくても実装可能かもしれないということを表そうとしています。


私のコードで問題が起こらないなら、なぜscala.Intにも++メソッドを設けなかったか?がまた、問題になると思います。
演算子もメソッドであると概念(?)は守りながら、恐らく長年愛されてきた++インクリメントをシンタックシュガーとしてでも取り入れればScalaでのコーディングが更に便利になるような気がします。

xuweixuwei 2014/02/09 17:44 NewtGeckoさん。
たしかにそういうやり方は不可能ではないですが、new MyIntと毎回やるのはダサすぎるし、パフォーマンスの問題(毎回余計なオブジェクトがnewされる)もあると思います。
あと、n ++ ができても、結局 ++ nの前置のインクリメントが不可能ですし。

また、"インクリメント"という、mutableな操作を入れなかったのは、(関数型言語の要素を持っているScalaとしては)そういうことは非推奨にしたいということで、意図的にそうしたのでしょう。

「whileとインクリメントの組み合わせ」は、大概は末尾再帰なメソッドで置き換えられるので、そういう書き方に慣れれば、そもそもインクリメントなんてあまり必要ありません

NewtGeckoNewtGecko 2014/02/09 20:51 早速のお返事ありがとうございます。

そもそも、Scalaをやる上で、代替法がありインクリメントの必要性が低いんですね。私のレベルではあまり気にならないパフォーマンスの面でも悪いんですね。よくわかりました。

親切にありがとうございました。

リンク元