LoginSignup
1
0

More than 5 years have passed since last update.

Programming in Scala 18 章 Mutable Objects

Last updated at Posted at 2017-05-30
1 / 15
  • 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 を返すので、高速化が期待できる。
    • 高速化を除いて、MemoKeyedKeyed の振る舞いは全く同じ。
    • もし、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 と 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 }
}

> 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。
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章を参照。
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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0