Scalaの代入式


トンデモ注意 追記2: 水島さんからコメントをもらった。ツッコミどころが多すぎて「こんな偽情報広めんなよアホ」という心持ちでタタタタタタと丁寧にコメント欄にご指摘をされたのだと思う。毎度すみません、というわけで言い訳っぽいコメントつきで掲示。

こんにちは。

> と、一見的外れな説明が書いてあるほかは var に関する説明は見当たらない (探し方が悪い?)。

についてですが、そのすぐ下にvariable definitionについて説明があります。

A variable definition var x: T = e introduces amutable variable with type T and initial value as given by the expression e. The type T can be omitted, in which case the type of e is assumed. If T is given, then e is expected to conform to it (§6.1).

ここで、ポイントなのが’declaration’ではなく’definition’であることで、Scalaの仕様書では両者は明確に区別されています。var x: Tはvariable declarationであり、variable declarationはローカル変数の宣言としては許されていないので、無関係です。
また、もし仮に以下のようなローカル変数xの定義によって、メソッドxとx_=が自動的に定義されるとすれば、

var x: Int = 10

はい、もちろん、すぐ下に説明があったのは知っていましたが、declaration の方は初期化なし、definition は初期化ありという意味ととらえていたので抜粋しませんでした。で、字面通りにとると、variable declaration の方は abstract な method が定義されると。なるほど、と思って次の実験をしてみました。

abstract class Foo { var x: Int }
abstract class Bar { def x: Int; def x_=(v:Int): Unit }
class C1 extends Foo {
  def x = 0
  def x_=(v: Int): Unit = {}
}
class C2 extends Bar {
  var x: Int = 0
}

C1, C2 どちらも定義できます。なので水島さんのおっしゃる通りです。

x _という式が許容されるはずですが、これはNGです。以上のことから、ローカル変数に対する代入が実はメソッド呼び出しである、ということは無いと言えると思います。


これについても追試しました。やっとけよって感じですが。

def x_=(v: Int): Unit = println(v)
x = 0 // NG

ですよね。

あと、

var a = 5
def foo(f: => Unit) = {}
foo(a += 1)
foo(a += 1)
foo(a += 1)
println(a) // a=?

についてですが、これは+=の解釈とは関係の無い話で、a += 1という(Unit型を返す)式が、by-name parameterを取るメソッドに渡されたため、評価が遅延し、しかも、メソッド本体ではby-name parameterが一切現れないので、a += 1が一回も評価されなかったという話です。


考えてみれば当たり前ですが、代入は Unit 型に評価される式だという単純な話でしたね。上記で代入はメソッドではないということは明らかになったので、式が暗黙的な変換でメソッド化して、by-name 渡しになったという解釈しかできません。単純に評価されていないということは、サンプルの意図した通りです (よもや代入がメソッドだから評価されないと言っていたわけではなく、メソッド呼び出しだから Unit 型の表現になるので by-name 渡しとなるというロジックのつもりだったのですが、説明がまずかったです。)

(万一読んでしまった方へ) というわけで一応かなり調べて書いてますが、この体たらくです。

…うーむ、例の syntatic sugar 自体の説明はどこにあるんだろうという話に戻ってしまった。

Scala の代入で仕様上ちょっと不明瞭なところがある。それは val と var における自己代入演算子の挙動の問題だ。

var l1 = List(1, 2, 3)
l1 += 4
println(l1) // 1, 2, 3, 4

List はイミュータブルなシーケンスなので "+=" というメソッドは実装していないが、この構文は正常に解釈される。これは、Scala には "+=" を "+" メソッドの呼び出しと変数への代入という 2 つの操作に自動的に分解してくれるような syntactic sugar があるからだ。

なお、var を val にすると、ある意味期待通りの動作をする。

val l2 = List(1, 2, 3)
l2 += 4 // ここで "+=" というメソッドがないというエラー
println(l2)

仕様を見てみる。

まず代入については、6.15 の Assignments で以下のように述べられている。

The interpretation of an assignment to a simple variable x = e depends on the definition of x . If x denotes a mutable variable, then the assignment changes the current value of x to be the result of evaluating the expression e . The type of e is expected to conform to the type of x . If x is a parameterless function defined in some template, and the same template contains a setter function x _= as member, then the assignment x = e is interpreted as the invocation x _=(e ) of that setter function. Analogously, an assignment f .x = e to a parameterless function x is interpreted as the invocation f .x _=(e).

An assignment f(args ) = e with a function application to the left of the “=" operator is interpreted as f.update(args, e ), i.e. the invocation of an update function defined by f.

6.15 Assignments - Scala Reference

それから var については

A variable declaration var x : T is equivalent to declarations of a getter function x and a setter function x _=, defined as follows:

def x : T
def x _= (y : T ): Unit

4.2 Variable Declarations and Definitions- Scala Reference

と、一見的外れな説明が書いてあるほかは var に関する説明は見当たらない (探し方が悪い?)。

ここで的外れと書いたのは、getter と setter という言葉からクラスのメンバとしての var を解説しているだけにしか読めないからだけど、Scala では def はローカルブロックの中に現れてもよいので、「ローカルブロックで var を定義すると、無名の記憶域が作成され、その記憶域へのアクセサが生成される」と拡大解釈することで、話として一応一貫性を保つことができる。

そうすると var で定義されるのはパラメータを取らないメソッドであるはずなので、メソッドとして別の変数に代入してやることも可能ではないかと思うのだが、どうなのだろう?

パラメータを取らないメソッドについては、次のような説明がある。まず、この部分。

A special case are types of methods without any parameters. They are written here => T. Parameterless methods name expressions that are re-evaluated each time the parameterless method name is referenced.

Method types do not exist as types of values. If a method name is used as a value, its type is implicitly converted to a corresponding function type.

3.3 Non-Value Types - 3.3.1 Method Types - Scala Reference
  1. パラメータを取らないメソッドは、そのメソッドの名前が参照されるたびに再評価されるような式に名前をつけたものとなる。
  2. メソッド型の値は存在せず、値として使われる場合は暗黙的に Function 型 (apply を定義するクラス) に変換される。

それから、

The following four implicit conversions can be applied to methods which are not applied to some argument list.


Evaluation.

A parameterless method m of type => T is always converted to type T by evaluating the expression to which m is bound.

Implicit Application.

If the method takes only implicit parameters, implicit arguments are passed following the rules of §7.2.

Eta Expansion.

Otherwise, if the method is not a constructor, and the expected type pt is a function type (Ts′ ) ⇒ T ′ eta-expansion (§6.25.5) is performed on the expression e .
Empty Application.

Otherwise, if e has method type ()T , it is implicitly applied to the empty argument list, yielding e ().

6.25 Implicit Conversions - 6.25.2 Method Conversions - Scala Reference
  1. パラメータを取らないメソッドは常にそのメソッドに結びつけられた式を評価した値に変換される
  2. 暗黙パラメータを持つメソッドには適合する暗黙パラメータが渡される
  3. イータ展開 (_ を使った式での無名関数の生成、call-by-name デリゲート、etc.)
  4. 空パラメータを取るメソッドでパラメータを省略しても自動的に指定したと見なす

ということなので

var a = 5
def foo(f: => Int) = f + 1
foo(a)

という式は当然有効である。

じゃあ、これはどうなるか。

def foo(f: => Int) = f + 1
foo(1)

イータ展開が暗黙的に行われるので OK だ。

で、最後にこれ。

var a = 5
def foo(f: => Unit) =  {}
foo(a += 1)
foo(a += 1)
foo(a += 1)
println(a) // a=?

にわかに信じられないが、a の値は 5 のままである。イータ展開されるため、引数として与えられた setter が呼ばれることはないからだ。

というわけで、最初の syntactic sugar の挙動と、実は代入もメソッドであるという話の両方が裏付けされたんじゃないかと思う。

追記: s/エータ展開/イータ展開/g