11.1 状態を持つオブジェクト (Stateful Object)

我々は世界をオブジェクトの集合としてとらえ、オブジェクトの中には時間と共に変化する状態を持つものもあります。通常、状態は計算の過程で変化しうる変数の集合と結びついています。プログラミング言語の特定の構文に触れずに、状態を抽象化して言うと、「もしオブジェクトの振る舞いがその履歴に影響されるなら、オブジェクトは 状態を持つ ( ステートフル である)。」となります。

たとえば、銀行口座オブジェクトは状態を持ちます。なぜなら「100スイスフラン引き出せるか?」は口座の存続期間、異なる答を持ちうるからです。

Scala ではすべてのミュータブルな状態は、結局のところ、変数から作られます。変数定義は値定義と同じように書きますが、val の代わりに var で始まります。たとえば、次の2つの定義は2つの変数、x と count を導入し初期化します。

var x: String = "abc" var count = 111

値定義と同様に変数定義は、名前と値を結びつけます。しかし変数定義の場合、この結びつきは後から代入によって変更できます。そのような代入は C や Java と同じように書きます。たとえば

x = "hello" count = count + 1

Scala では定義する変数はすべて、定義の時点で初期化しければなりません。たとえば文、 var x: Int; は変数定義とみなされません。なぜなら初期値が無いからです(*2)。適切な初期値を知らない、あるいは気にしない場合は、代わりにワイルドカードを使えます。たとえば、

val x: T = _

は、x を何かデフォルト値 (参照型には null を、論理型には false を、数値型には適切な 0 を) で初期化します。

実世界のオブジェクトは、Scala では変数をメンバに持つオブジェクトとして表現されます。たとえば銀行口座を表現するクラスです。

class BankAccount { private var balance = 0 def deposit(amount: Int) { if (amount > 0) balance += amount } def withdraw(amount: Int): Int = if (0 < amount && amount <= balance) { balance -= amount balance } else error("insufficient funds") }

このクラスは、口座の現在の残高を入れておく変数 balance を定義しています。メソッド deposit と withdraw は、代入によってこの変数の値を変更します。クラス BankAccount において、balance が private であることに注意して下さい。この結果、クラスの外からは直接アクセスできません。

銀行口座を作成するには、通常のオブジェクト生成の記法を使います。

val myAccount = new BankAccount

例 11.1.1 次は銀行口座を扱う scala セッションです。

scala> :l bankaccount.scala Loading bankaccount.scala... defined class BankAccount scala> val account = new BankAccount account: BankAccount = BankAccount$class@1797795 scala> account deposit 50 unnamed0: Unit = () scala> account withdraw 20 unnamed1: Int = 30 scala> account withdraw 20 unnamed2: Int = 10 scala> account withdraw 15 java.lang.Error: insufficient funds at scala.Predef$error(Predef.scala:74) at BankAccount$class.withdraw(<console>:14) at <init>(<console>:5) scala>

この例は、同じ操作 (withdraw 20) を口座に2回適用して、異なる結果が生じることを示します。したがって口座がステートフルなオブジェクトであることは明らかです。

同一性と変化 代入は2つの式が「同じ」かを判定する際に新しい問題をもたらします。もし代入が排除されている場合には、次のよう、

val x = E; val y = E

ただし E は何か任意の式であるとする、と書いた場合、x と y は無理なく同じと考えられます。つまり次のように書いても等価です。

val x = E; val y = x

(この性質は通常、 参照透過性 と呼ばれます) しかしもし代入を許したなら、上の2つの定義は異なります。次を考えて下さい。

val x = new BankAccount; val y = new BankAccount

この問題、x と y は同一であるか、に答えるには、「同一」の意味をより明確にしなければなりません。この意味は 操作的等価性 としてとらえられ、くだけた言い方をするなら、次のようになります。

仮に x と y の2つの定義があるとします。x と y とが同じ値を定義しているか調べるには、次のようにします。

    • 定義を実行し、引き続いて x と y とを含む任意の操作列 S を実行し、(もしあれば) 結果を調べる。

    • 次いで、定義を実行し、S 中のすべての y の出現を x で置き換えた、S から得られた列 S' を実行する。

    • もし S' を実行した結果が異なれば、x と y は確かに異なる。

    • もしすべての可能な操作列の組 {S, S'} に対して、同じ結果が得られるなら、x と y は同一である。

別の言い方をすれば、操作的等価性は2つの定義 x と y を、もしどんな実験でも x と y を区別できないなら、同じ値だとみなします。この文脈における実験とは、x あるいは y を用いる任意の2つのプログラムです。

この定義を用いて、

val x = new BankAccount; val y = new BankAccount

が、値 x と y を等しく定義するか確かめましょう。定義を再度行い、次いでテスト列を。

> val x = new BankAccount > val y = new BankAccount > x deposit 30 30 > y withdraw 20 java.lang.RuntimeException: insufficient funds

さて、y のすべての出現を x に置き替えます。すると

> val x = new BankAccount > val y = new BankAccount > x deposit 30 30 > x withdraw 20 10

最終結果が異なるので、x と y が異なることが証明されました。その一方で、もし

val x = new BankAccount; val y = x

と定義すると、どんな操作も x と y を区別できません。したがってこの場合、x と y は等しくなります。

代入と置き換えモデル これらの例が示すことは、以前の計算の置き換えモデルは、もう使えないということです。結局のところ、このモデルでは変数の名前を定義式で常に置き換え可能なのですから。たとえば

val x = new BankAccount; val y = x

の、y の定義中の x は new BankAccount で置き換え可能です。しかしその変更は異なるプログラムを導くことを見てきました。したがって置き換えモデルは、代入を加えると、妥当ではありません。

(*2) もしこのような文がクラス中に表れたなら、そうではなく変数宣言とみなされ、変数に対する抽象アクセスメソッドを導入しますが、これらのメソッドを状態とは結びつけません。