LL脳がscalaの勉強を始めたよ その62


Scalaコップ本の18章に入っていきますよー、18章はステートフルオブジェクトに関するアレやコレですねー

どのようなオブジェクトがステートフルなのか

そもそもステートフルオブジェクトってなんじゃらホイ?ってはないですな。ステートフルオブジェクトと比較されるのが純粋関数型オブジェクトで、要するにイミュータブルな世界のことを表すようです。

純粋関数型オブジェクト

純粋関数型オブジェクトはメソッドの呼び出しやフィールドの参照時に、必ず同じ値が結果として返ってくるものらしいデス。コップ本で散々おすすめしてきたのがこいつですね。

乱暴に言ってしまうとvalで定義したイミュータブルなものって解釈でいいかしらネ

// val定義のcsの値はずっと変わらないので保証されまっす
scala> val cs = List(1,2,3,4,5)
cs: List[Int] = List(1, 2, 3, 4, 5)

// csの先頭値をとるheadの結果も常に保証されますねー
scala> cs.head
res0: Int = 1
それじゃステートフルオブジェクトは?

純粋関数型オブジェクトに対して、メソッド呼び出しやフィールドの参照結果がそれまでの操作の結果によって変わってしまうのがステートフルオブジェクトみたいです。

例えばステートフルオブジェクトのサンプルは次のような感じになるみたいですね。サンプルは銀行口座をモデルにしたクラスです(`・ω・´)

// 銀行口座クラス
class BankAccount{
  // 残金を表す非公開フィールド、ミュータブルです
  private var bal:Int = 0
  // 残金を返すメソッド
  def balance:Int = bal
  // 預入を行うメソッド
  def deposit(amount:Int){
    // 負の値は預けられないように、負数を渡されたら
    // .IllegalArgumentException例外を投げますデス
    require(amount > 0)
    // 残金を増やします
    bal += amount
  }
  // 引き出しを行うメソッドです
  def withdraw(amount:Int):Boolean = {
    // 残金より大きい値を引き出そうとしたらfalseを返します
    if(amount > bal) false
    // 残金以内なら残金を減らしてtrueを返します
    else {
      bal -= amount
      true
    }
  }
}

//// 実行してみますよー
// 銀行口座を開設します
scala> val account = new BankAccount
account: BankAccount = BankAccount@187e184

// 預入をしますよー 
scala> account.deposit(100)
// 残金が増えましたー
scala> account.balance     
res8: Int = 100

//  引き出します
scala> account.withdraw(80)
res10: Boolean = true
// 引き出されましたー
scala> account.balance     
res11: Int = 20

// 残金以上に引き出してみますよ
scala> account.withdraw(80)
// 引き出せませんでした
res12: Boolean = false
// 残金もそのままです
scala> account.balance     
res13: Int = 20

こんな感じでvarを使っているのでステートフルオブジェクトとみなすことが出来るらしいです。(varを使っているからといって必ずしもステートフルにはならないそうですが…)

ちょっと気になったので

銀行口座開設時にお金を預けられないのは不便なので BankAcountクラスを改造してみますよ。ええ、ただの自己満足です(`・ω・´)

// コンストラクタのnew時に口座にお金を入れますよ
class BankAccount(private var bal:Int){
  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
    }
  }
}

本当はデフォルト金額を指定してパラメータ無しでも使えるようにしたかったんだけど、デフォルト引数みたいなのはscala2.8からみたいですね(´・ω・`)クラスコンストラクタに使えるのかはわからないけど…あとで試してみよう。ちなみに2.7系までは多重定義でなんとかするんですね。

var を使いつつも純粋関数型なオブジェクト

必ずしも「var = ステートフル」となるわけではなく、varをつかってなくてもステートフルオブジェクトになるものもあるし、varを使っていても純粋関数型になるものもあるそうです。コップ本には世の中はそんなに簡単なものでないですから( ー`дー´)キリッって具合の無常観っぽいことが書いてありまった。

とりあえずそんな例外的な存在としてvarを含みつつも純粋関数型となるようなサンプルをやってみますかねー。例えば処理のキャッシュとしてvarを使うものそうみたいです。

例えば次のような時間のかかる処理メソッドを持つクラスを考えます

class Keyed{
  def computeKey:Int = {// 死ぬほど時間のかかる処理}
}

上記のような時間のかかる処理(返す結果は毎回同じ)を何度も実行するのは時間の無駄になるので、上記クラスを継承して時間のかかる処理の結果をvarを使ってキャッシュするような改造を施すサブクラスを書いてみますよ

// 結果キャッシュを行うKeyedクラスのサブクラスです
class Memokeyed extends Keyed {
  // キャッシュがない場合はNoneを格納するためOption型を使いますね
  private var keyCashe:Option[Int] = None
  // 重い処理を継承して改造しますよ
  override def computeKey:Int = {
    // キャッシュがない場合がスーパークラスのメソッドで
    // 実際の処理を行なって結果を取得して結果はキャッシュします
    if(!keyCache.isDefined) keyCache = Some(super.computeKey)
    // キャッシュから結果値を取得します
    keyCache.get
  }
}

上記例はvarを使ってはいるもののキャッシュにしか使用していないため、結果値は常に一定になるので純粋関数型であるといえるみたいです。

再代入可能な変数とプロパティ

Scalaではオブジェクトに対して公開・限定公開のめんばーとして再代入可能な変数が定義された場合はゲッター、セッターメソッドが自動的に定義されるみたいですね。ちなみにゲッターは<フィールド名>、セッターは<フィールド名>_=の形式で定義されるみたいです。例えば公開されたミュータブルフィールドsomethingならゲッターがsomethingでセッターがsomething_になるみたいです

ちょっとしたサンプルを書いてみますよー

class Time{
  var hour = 12
  var minute = 0
}

//// ちょっといじってみますね
// オブジェクトを生成しますよ
scala> val time = new Time
time: Time = Time@1afe17b

// ゲッターです
scala> time.hour          
res21: Int = 12

// セッターです
scala> time.hour_=(15)
// 値がセットされました
scala> time.hour
res23: Int = 15

// 次のような書き方はセッターの省略形ですかねー
scala> time.hour = 15     
セッター・ゲッターの詳細定義

実際のところ上の書き方は下のような詳細定義と同じ意味になるそうです

class Time{

  // 完全非公開のフィールドを定義します
  private[this] var h = 12
  private[this] var m = 0

  // hourに関するsetter/getterです
  def hour:Int = h
  def hour_= (x:Int){h= x}

  // minuteに関するsetter/getterです
  def minute:Int = m
  def minute_=(x:Int){m=x}
}
セッター・ゲッターの直接定義

上記のような詳細定義を利用することでセッターやゲッターに条件を付けることができます。例えばTimeクラスであればhourは0~23まででminuteは0~59までしかセッターのパラメータとして取らないYO、とかの定義ができますね

class Time{
  private[this] var h = 12
  private[this] var m = 0

  def hour:Int = h
  // hourに関するsetterに条件を定義します
  def hour_= (x:Int){
    require(0 <= x && x < 24)
    h = x
  }
  
  def minute:Int = m
  // minuteに関するsetterに条件を定義します
  def minute_=(x:Int){
    require(0 <= x && x < 60)
    h = x
  }
}

//// ちょっくらためしてみますよー
// Timeオブジェクトを生成します
scala> val time = new Time
time: Time = Time@c3bb57

// hourに値を突っ込んでみます
scala> time.hour = 21
// 条件外の値を代入するとエラー出まくりですねー
scala> time.hour = 29
java.lang.IllegalArgumentException: requirement failed
	at scala.Predef$.require(Predef.scala:107)
	at Time.hour_$eq(<console>:12)
	at .<init>(<console>:7)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethod...

// minuteに値を代入しますよ
scala> time.minute = 29
// こちらもrequireで条件外をキャッチしています
scala> time.minute = 129
java.lang.IllegalArgumentException: requirement failed
	at scala.Predef$.require(Predef.scala:107)
	at Time.minute_$eq(<console>:19)
	at .<init>(<console>:7)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMeth...

セッター・ゲッターが特別に定義できることで上記の例のようにセットする値に条件を付与したり、変更された値の内容をログに出力したり、変数が変更されるたびにイベントを発生させたりできるみたいです。最後の例は33 章でやるみたいですねー。

基本的に普段使う変数は暗黙的に生成されるセッター・ゲッターを利用して、上記のような特殊な用途の場合は直接定義を使うのがヨサゲな感じですねー。ちなみにJavaBeansなんかだと各変数についてセッター・ゲッターを明示的に定義する必要があるらしく、セッター・ゲッター地獄に陥るとのこと…それはイヤダ(n‘Д‘)η゚

対応するフィールドがないセッターとゲッター

Scalaでは対応するフィールドがないセッターやゲッターを定義することが出来るみたい、しかもすげー便利とのこと…なんじゃそりゃ?っと思ったのでまずはサンプルからやってみますかね。サンプルは温度変数を摂氏と華氏の両方で読み書き出来るようにしたものですねー

// 温度管理クラスです
class Thermometer{
  // 摂氏の温度を格納します
  // 初期値"_"が指定されているので型ごとの初期値(Floatなら0)が入力されます
  // 正確には=_という初期化子の働きらすぃです(´・ω・`)
  // ちなみに =_を省略すると抽象変数宣言になるので省略できないとのこと
  // 抽象変数変幻については20章でやりマス
  var celsius: Float = _
  
  //// 華氏に関するセッター・ゲッターです
  // セッターでは摂氏を華氏に変換して値を返します
  def fahrenheit = celsius * 9 / 5 +32
  // ゲッターでは華氏の値を摂氏に変換してcelsiusに格納します
  def fahrenheit_=(f:Float){
    celsius = (f - 32) * 5/ 9
  }
  // 格納された温度を摂氏と華氏の両方で表示します
  override def toString = farenheit + "F/" + celsius + "C"
}

んじゃ、とりあえず実行してみますよー

// オブジェクトを生成しますよね
scala> val thermo = new Thermometer
thermo: Thermometer = 32.0F/0.0C

// 摂氏ゲッターです
scala> thermo.celsius
res27: Float = 0.0

// 摂氏セッターですよー
scala> thermo.celsius = 100
// 結果です
scala> thermo              
res28: Thermometer = 212.0F/100.0C

// 華氏ゲッターです
scala> thermo.fahrenheit
res30: Float = 212.0

// 華氏セッターを使います
scala> thermo.fahrenheit = 40 
// 結果はこのとおりです
scala> thermo                
res31: Thermometer = 40.0F/4.4444447C


上記のようにフィールドを定義せずにセッターとゲッターのみを定義することで、一つのデータに対して複数の表現型(INPUTとOUTPUTの口)を用意することが出来るわけですね。なるほど(´・ω・`)確かにこりゃ便利だわ

いじょー

今回はここまでー、次回はステートフルオブジェクトのケーススタディーをやりますよ