- 17章は、関数型(immutable; 不変)オブジェクトにフォーカスを当てた。
- しかし、何度も変更される現実世界のオブジェクトをモデリングする際は、mutable(可変)オブジェクトはよく登場する。
- 本章では、mutable オブジェクトとは何か、Scala が mutable オブジェクトを表現する上で提供するシンタックス、mutable オブジェクトを使った大きめのケーススタディ(ディジタル回路シミュレーションの DSL)を紹介する。
18.1 何がオブジェクトを可変(mutable)にするか?
- 純粋関数型オブジェクト(あるいは、不変オブジェクト)上のメソッド呼び出し、あるいは、フィールドを参照すると、常に同じ値
- 以下の例では、
cs.head
が指す先は常にa
val cs = List('a', 'b', 'c')
- 可変オブジェクトの場合、メソッド呼び出し、あるいはフィールドの値は、前回、どのような操作が行われたかで、結果が異なる。
- 例として、簡易的な銀行口座の実装を示す。
- この例は、クラス定義に var なフィールドを持つため、可変であることが分かりやすい。
18.1
import scala.language.postfixOps
class BankAccount {
private var bal: Int = 0
def balance: Int = bal
def deposit(amount: Int) = {
require(amount > 0)
bal += amount
}
def withdraw(amount: Int): Boolean = {
if (amount > bal) false
else {
bal -= amount
true
}
}
}
- (筆者注)書籍のソースコードは
account balance
を実行すると警告が出るため、1行目のimportを追加した。- 警告「warning: postfix operator balance should be enabled by making the implicit value scala.language.postfixOps visible. This can be achieved by adding the import clause 'import scala.language.postfixOps' or by setting the compiler option -language:postfixOps.」
- (参考)https://stackoverflow.com/questions/13011204/scalas-postfix-ops/13011301
- postfix operation notation は以下のようなもの。
List(1,2,3) tail
- 最後の 2 つの withdaw は異なる結果を返している。
- 1 回目は引き出しに十分なお金があったため true
- 2 回目は不十分なため false
- 同じ操作に対して異なる結果を返すことがあるため、この銀行口座は可変(mutable)な状態を持っている。
scala> val account = new BankAccount
account: BankAccount = BankAccount@3e7545e8
scala> account deposit 100
scala> account balance
res1: Int = 100
scala> account withdraw 80
res2: Boolean = true
scala> account withdraw 80
res3: Boolean = false
- var があれば、すなわち可変というわけではない。
- 直感に反するケースが定義できる。
- var を定義したり、継承しなくても可変
- var 含んでいても、純粋関数型(不変)
- var を含んでいても、純粋関数型(不変)な例
- クラス
Keyed
は計算コストがかかる操作computeKey
を含む。 - クラス
MemoKeyed
では、キャッシュkeyCache
を用意することで、より効率的。 - 2回目に
computeKey
が要求された場合、新たにcomputeKey
を計算することなく、keyCache
を返すので、高速化が期待できる。 - 高速化を除いて、
MemoKeyed
とKeyed
の振る舞いは全く同じ。 - もし、
Keyed
が純粋関数型である場合、再代入可能な変数keyCache
を持つにもかかわらず、MemoKeyed
も純粋仮想型。
- クラス
class Keyed {
def computeKey: Int = ... // この計算には時間がかかる
}
class MemoKeyed extends Keyed {
private var keyCache: Option[Int] = None
override def computeKey: Int = {
if (!keyCache.isDefined) keyCache = Some(super.computeKey)
keyCache.get
}
}
18.2 再代入可能な変数とプロパティ
- 再代入可能な変数に実行できる操作
- 値を取得する
- 値を設定する
JavaBeans のようなライブラリでは、これらはよく getter と setter という別の操作にカプセル化されている。
Scalaでは、全ての非 private な var には、暗黙的に setter と getter が生成される。
-
命名規則:例 var hour = 12
- getterの名前は
hour
- setterの名前は
hour_=
- getterの名前は
getter と setter の可視性は元の var と同じ。
次に public な var が setter と getter メソッドに展開される例を示す。
18.2
class Time {
var hour = 12
var minute = 0
}
- 上記のクラス定義は実際には以下と等価
- (注)名前の衝突を防ぐため、ローカル変数の名前は短縮している。
18.3
class Time {
private[this] var h = 12
private[this] var m = 0
def hour: Int = h
def hour_= (x: Int) = { h = x }
def minute: Int = m
def minute_= (x: Int) = { m = x }
}
- (筆者注)private[this] はそのインスタンスからのみアクセスできるようにする可視性。
- (参考)https://stackoverflow.com/questions/9698677/privatethis-vs-private
> val t = new Time
t: Time = Time@4e904fd5
> t.h = 34
<console>:12: error: value h is not a member of Time
t.h = 34
^
>:14: error: value h is not a member of Time
val $ires0 = t.h
- var を定義せず、直接 setter、getter を定義することもできる。これにより、変数へのアクセスや代入の操作を好きなように定義できる。
18.4
class Time {
private[this] var h = 12
private[this] var m = 0
def hour: Int = h
def hour_= (x: Int) = {
require(0 <= x && x < 24)
h = x
}
def minute: Int = m
def minute_= (x: Int) = {
require(0 <= x && x < 60)
m = x
}
}
- (筆者注)require には、引数が正当な値の範囲に収まっているかチェックするためのメソッド。条件を満たさないとき、例外(IllegalArgumentException)を発生させる。Predef オブジェクトで定義されており、暗黙的にインポートされているため、デフォルトで使える。
- setter で値をチェックし、異常値であれば例外を発生する。
> val t = new Time
t: Time = Time@5b3a7ef5
> t.hour
res2: Int = 12
> t.hour = 34
java.lang.IllegalArgumentException: requirement failed
at scala.Predef$.require(Predef.scala:264)
at Time.hour_$eq(<console>:17)
... 29 elided
> t.minute
res4: Int = 0
> t.minute = 65
java.lang.IllegalArgumentException: requirement failed
at scala.Predef$.require(Predef.scala:264)
at Time.minute_$eq(<console>:22)
... 29 elided
- Scalaの、変数を常に setter と getter のペアだと解釈する慣習により、特別な構文なしに、C# の property と同じ機能が利用できる。
- (参考)Using Properties (C# Programming Guide)
- C# による以下のコードでは、変数 month の getter と setter を定義したことと同じ意味になる。
public int Month
のブロックが property。
- C# による以下のコードでは、変数 month の getter と setter を定義したことと同じ意味になる。
public class Date
{
private int month = 7; // Backing store
public int Month
{
get
{
return month;
}
set
{
if ((value > 0) && (value < 13))
{
month = value;
}
}
}
}
- property は様々な目的を果たす。
- setter で不変条件を強制する(不正な値の設定を防ぐ)・・・18.4 の例(Time に独自定義の setter と getter をつけたもの)
- 全ての getter や setter へのアクセスに対してログを残す。
- イベントと変数を統合する・・・(例)変数が変更される度に subscriber に通知する。35章参照。
- 関連するフィールドなしに、役に立つ getter と setter を定義することも可能。
- 18.5 の例では、フィールドとしては celsius(セ氏の温度)を持ち、華氏で温度を設定する setter と華氏で温度を取得する getter を持つ。温度を華氏で持つフィールドはない。
- celsius に対する
= _
は、その型の初期値を代入することを意味する。数値型であれば 0、boolean であれば false、参照型であれば null。 - (注)
var celsius: Float
だけだと未期化ではなく、抽象変数になる。20章を参照。
- celsius に対する
18.5
class Thermometer {
var celsius: Float = _
def faurenheit = celsius * 9 / 5 + 32
def faurenheit_= (f: Float) = {
celsius = (f - 32) * 5 / 9
}
override def toString = faurenheit + "F/" + celsius + "C"
}
scala> val t = new Thermometer
t: Thermometer = 32.0F/0.0C
scala> t.celsius = 100
t.celsius: Float = 100.0
scala> t
res0: Thermometer = 212.0F/100.0C
scala> t.faurenheit = -40
t.faurenheit: Float = -40.0
scala> t
res1: Thermometer = -40.0F/-40.0C