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


Scalaコップ本の18章の続きをやっていきますよー、今回はステートフルオブジェクトのサンプルとして実装するデジタル回路シミュレーターのシミューレションAPIからですよー

シミュレーションAPI

まずはシミュレーションを行う具象ライブラリが継承するSimulation抽象クラスを定義してみますよー、まずはシミュレーションAPIで行う要素をザザっと並べていきますよ

離散イベントシミュレーションの実行定義

離散イベントシミュレーション指定されたタイミングでユーザ定義のアクションを実行するので、次のような型を共有することで定義するそうです。

// 空パラメータをとってUnitを返す副作用メソッドデス
type Action = () => Unit

上記で定義されたActionは型メンバーと呼ばれるらしいのですが、型メンバーについてh20章でやるそうですねー

シミュレーション中の時間表現

シミュレーション上での時間は実時間と異なってライブラリの中で管理されるので、外部から変更されないようにprivateな変数として管理しますね。

// 初期値は開始時なので0デス
private var curtime:Int = 0

privateな変数なのでアクセサメソッドも必要っすね

// curtime用のアクセサです 
def currentTo,e:Int = curtime

これらの時間を利用してユーザ定義アクション(作業項目)の実行タイミングを測るみたいです

実行されるアクションの定義

指定されたタイミングで実行されるユーザ定義アクション(作業項目)はケースクラスとして定義しますね。ケースクラスで定義するとファクトリーメソッドと、コンストラクタパラメータに対するアクセサが追加されるみたいです。

// ファクトリーメソッドWorkItemと
// time, actionのへアクセサが手に入りますネ
case class WorkItem(time:Int, action:Action)

実際のところ大外のクラス(Simulationクラス)の中に入れ子構造でWorkItemクラスを定義するのですが、Scala入れ子クラスはJavaのソレと同じなんだとか…詳しくは20章だそうです(´・ω・`)

作業項目の予定表

シミュレーションクラスはまだ実行されていない全てのユーザ定義アクション(WorkItem)を予定表として管理するのです。WorkItemは実行すべき(シミュレーション上の)時間順にそーとされるみたいですねー

// 初期はWorkItemが何もないのでからリストです
private var agenda:List[WorkItem] = List()
予定表への作業項目の追加とソート

ちなみに予定表のソートはWorkItemの追加時にinsertメソッドで行うそうです。

// 追加&ソート処理ですよ
private def insert(ag:List[WorkItem], item:WorkItem):List[WorkItem] = {
  // 先頭の作業項目よりも実行時間が早い場合は先頭に
  if(ag.isEmpty || item.time < ag.head.time) item :: ag
  //  再帰的に実行することで(実行時間的に)適切な位置に追加しますよ
  else ag.head :: insert(ag.tail, item)
}

再帰での位置探索はスッキリ書けるなぁ(´・ω・`)

遅延後の追加処理

追加メソッドinsertははシミュレーションの遅延時間後にとして行われますな(`・ω・´)

// 遅延処理後に実行されるメソッド
def afterDelay(delay:Int)(block: => Unit){
  // 追加Item(ユーザ定義アクション)を生成
  val item = WorkItem(currentTime + delay, () => block)
  // 予定表への追加処理はinsert処理で行う
  agenda = insert(agenda, item)
}

ちなみにafterDelayの第2パラメータはUnit型の関数が入るので次のような呼出になるようです

// currentTime + delay秒にcount変数をインクリメントする処理を実行しますです
// カリー化のせいで見た目はメソッド定義です
after(delay){count += 1}

どうやらカリー化を行うことでメソッド呼び出しなのに関数呼び出しに見えるようにしているっぽいですな。

シミュレーションの実行メソッド

Simulationクラスの実行定義をしますよ。実行を制御するrunメソッドでは作業予定表が空になるまでに実際に作業項目の実行を行うnextメソッドを実行しますねー

def run(){
  // シミュレーション開始を宣言
  afterDelay(0){
    println("*** シミュレーション開始, time = "+ currentTime + " ***")
  }
  // 予定表にデータが存在する限り実行しますよ
  while(!agenda.isEmpty) next()
}

実際の実行を担うnextメソッドは次のように書きますねー。runメソッドで空リスト(予定表がない場合)での実行は防いでいるのでパターンマッチでは書いていないのですが、コンパイラ様はそこら辺の事情を考慮せずにMatchError例外(全てのパターンが網羅されてねーYO!)が投げられるのでuncheckedアノテーションを付与しますね。

private def next() {
  // 空リストが存在しないエラーを防ぐためにuncheckedアノテーションを付与
  (agenda: @unchecked) match {
    // 予定表から次の作業項目を取り出します
    case item :: rest => {
      // 該当作業項目を取り除いた予定表を新たな予定表として定義しますです
      agenda = rest
      // 実行時間を現在時間として上書きしますね
      curtime = item.time
      // 作業項目のユーザ定義アクションを実行しますよ
      item.action()
    }
  }
}

シミュレーション定義は以上ですね(´・ω・`)実際は作業項目の実行順序制御がほとんどですなー

Scalaコードの実装

それじゃ、ここまで書いてきた定義コードを並べていきますよー

// com.test.simulationパッケージとして宣言
package com.test.simulation

// Simlation抽象クラスを定義 
abstract class Simulation {
  // ユーザ定義アクションの実行定義デス
  type Action = () => Unit
  // ケースクラスの定義デス
  case class WorkItem(time:Int, action:Action)
  // シミュレーション時の時間定義
  private var curtime = 0
  def currentTime:Int = curtime
  // 作業予定表の定義
  private var agenda: List[WorkItem] = List()
  // 作業予定表への作業項目の追加
  private def insert(ag:List[WorkItem], item:WorkItem):List[WorkItem] = {
    if(ag.isEmpty || item.time < ag.head.time) item :: ag
    else ag.head :: insert(ag.tail, item)
  }
  // 遅延後に作業項目を追加します処理です 
  def afterDelay(delay:Int)(block: => Unit){
    val item = WorkItem(currentTime + delay, () => block)
    agenda = insert(agenda, item)
  }
  // 作業項目を実行しますよー 
  private def next() {
    (agenda: @unchecked) match {
      case item :: rest => {
        agenda = rest
        curtime = item.time
        item.action()
      }
    }
  }
  // 作業実行制御ですね
  def run() {
    afterDelay(0) {
      println("*** シミュレーション開始, time = "+ currentTime + " ***")
    }
    while(!agenda.isEmpty) next()
  }
}

いじょー

とりあえず時間切れでココマデです。復習内容がガンガンでてきて面白いっすねー、次回は実際のデジタル回路シミュレーションを書いていきますよー。