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


Scalaコップ本33章の続きをやっていきますよ、33章は今回で最後のコップ本総まとめのスプレッドシートアプリケーションデス。今回は数式対応の総まとめと仕上げをやりたいと思います(`・ω・´)

値の変更の通知

前回までに構築したスプレッドシートアプリケーションでは、数式の演算自体はできるようになったものの、数式で利用した他のセルの値が変更してもすでに数式が入力されたセルの値が変更されないという問題がありますデス。なので、セルの値が変更された場合に再計算する仕組みを考えていく必要があるのデス(`・ω・´)

再計算方法の検討

単純に考えれば「セルの値が変更→スプレッドシート内の各セルの値を全て再評価」という方法があるのですが、この方法ではスプレッドシートが大きくなればなほど計算量が膨大に増えてしまうので、スケーラブルでないのでこの方法は無しですな(`・ω・´)

なので、とあるセルが変更された場合にそのセルを参照している数式のあるセルのみ変更するという仕組みを考える必要がありますデス。

イベント駆動の利用

「何かが変更された場合に何らかのアクションを実行する」という振る舞いはパブリッシュ・サブスクライブ(発行・購読)フレームワークを利用するのが良さ気なのでこいつを導入してみます。

イベントの管理としてはこんな感じの流れになりますかね(`・ω・´)

  • セルに数式が入力されたときにその数式が参照している全てのセルに対して、値の変更を通知するように購読の登録を行う
  • 該当のセルのどれかで値が変更されると購読者セルの再評価が実行
  • 再評価の結果でさらに他のセルの値が変わる場合は、そのセルに依存するセルにも通知を発行
  • セルの変更が行われなくあるまで上記を繰り返す

上記動作を実現するにはScalaのswingフレームワークの標準イベントメカニズムをModelクラスに組み込む必要があります(`・ω・´)ので、やってみます。

package org.test.scells
// swingパッケージをインポートします
import swing._
class Model(val height:Int, val width:Int)
  extends Evaluator with Arithmetic {
  // Publisherを継承して発行者として振る舞います
  case class Cell(row:Int, column:Int) extends Publisher {
    // toStringメソッドはそのままデス
    override def toString = formula match {
      case Textual(s) => s
      case _ => value.toString
    }
    //// value関連メソッドを定義します
    //// クラスの外からはタダのvalueフィールドだけども
    //// setter時にpublisherの仕事もシレッと行います
    // 非公開メンバーとしてvalueの値を定義
    private var v:Double = 0
    // ゲッターを定義します
    def value:Double = v
    // セッターを定義します
    def value_=(w:Double) {
      // 変更前の値と変更後の値が等しくない(空同士でもない)場合に
      // 値のセット処理を実行
      if(!(v == w || v.isNaN && w.isNaN)){
        v = w
        // 値のセットと同時にValueChangedイベントを発行します
        publish(ValueChanged(this))
      } 
    }
    //// formula関連メソッドを定義します
    //// クラスの外からはタダのformulaフィールドだけども
    //// setter時にsubscriberの仕事もシレッと行います
    // 非公開メンバーとしてformulaの値を定義
    private var f:Formula = Empty
    // ゲッターを定義します
    def formula:Formula = f
    // セッターを定義します
    def formula_=(f:Formula) {
      //// セルに新しい数式が代入された場合の処理
      // それまでの数式が参照していた全てのセルに対して
      // 購読解除を実行
      for(c <- references(formula)) deafTo(c)
      // セル内の数式を新しいものに置き換え
      this.f = f
      // 新しい数式が参照する全てのセルにイベント通知の購読を登録します
      for(c <- references(formula)) listenTo(c)
      value = evaluate(f)
    }
    // セル値変更イベントを受け取った際のアクションを定義します
    reactions += {
      // 変更イベントが発生したら数式を再評価シマス
      case ValueChanged(_) => value = evaluate(formula)
    }
  }
  // ValueChangedクラスはケースクラスとしてModel内に定義してやります(´・ω・`)
  case class ValueChanged(cell:Cell) extends event.Event
  // 他は今まで通りです
  val cells = new Array[Array[Cell]](height, width)
  for (i <- 0 until height; j <- 0 until width)
    cells(i)(j) = new Cell(i, j)
}

Modelへの組み込みはこんな感じで完成ですな(`・ω・´)

Spreadsheetクラスへの再描画処理の追加

Cell内部の値の変更に追随してセルの表示変更を行う処理も追加してやります。何も考えずにModelクラスのvalue_=セッターに再描画処理(redrawコマンド)を追加してしまう…というのもあるのですが、モデルと表示系を分離するべき!というモジュラースタイルを踏襲してGUI部分であるSpreadsheetクラスに再描画処理を追加してやります。


Spreadsheetクラスに追加するのはイベント購読の登録と、セル内データの変更イベントが発生した場合のイベントアクション処理の追加デスネ(`・ω・´)コードとしてはこんな感じになりますデス

package org.test.scells
import swing._
import event._
class Spreadsheet(val height:Int, val width:Int) extends ScrollPane {
  val cellModel = new Model(height, width)
  import cellModel._  
  val table = new Table(height, width){
    rowHeight = 25
    autoResizeMode = Table.AutoResizeMode.Off
    showGrid = true
    gridColor = new java.awt.Color(150, 150, 150)
    override def rendererComponent(isSelected:Boolean,
        hasFocus:Boolean, row:Int, column:Int):Component =
      if(hasFocus) new TextField(userData(row, column))
      else
        new Label(cells(row)(column).toString){
          xAlignment = Alignment.Right
        } 
    def userData(row:Int, column:Int):String = {
      val v = this(row, column)
      if(v == null) "" else v.toString
    }
    reactions += {
      case TableUpdated(table, rows, column) =>
        for(row <- rows)
          cells(row)(column).formula = 
            FormulaParsers.parse(userData(row, column))
      // セルの値が変更された場合の処理を追加
      case ValueChanged(cell) =>
        // セルの表示を再描画するupdateCellメソッドを呼び出し
        updateCell(cell.row, cell.column)
    }
    // 全てのセルに対してイベントの購読を登録
    for(row <- cells; cell <- row) listenTo(cell)
  } 
  // 
  val rowHeader = new ListView(0 until height){
    fixedCellWidth = 30
    fixedCellHeight = table.rowHeight
  }
  viewportView = table
  rowHeaderView = rowHeader
}

…と組み込み終わったのでコンパイルして実行してみますよ(`・ω・´)

// コンパイル
$ scalac *.scala
// 実行
$ scala org.test.scells.Main

うん、想定道理のセル編集の反映アクションをするようになりました(`・ω・´)

まとめ

とりあえずコップ本的総まとめのスプレッドシートアプリケーションはこれにて終了です。また読者に向けてのコップ本的課題として上記スプレッドシートアプリケーションの改造項目が幾つか上がってますね(´・ω・`)

  • スプレッドシートのサイズを変更できるようにすれば、行や列の数を対話的に変更できるようにする
  • 2項演算、その他の関数など、新しい数式を追加する
  • セルが再帰的に自分自身を参照していた場合への対処
    • A1セルがadd(B1, 1)という数式を保持していてB1がmul(A1, 2)という数式を保持している場合、どちらかのセルの再評価をはじめるとスタックオーバーフローを引き起こすので、これに対処する必要あり
    • どちらかのセルが書き換えられるたびに1度ずつ反復処理して終わるようにするとか?
  • エラー処理を拡張して何がまずかったのかを適切に説明できるようにする
  • スプレッドシートの上部に数式フィールドを追加して、長い式を入力しやすくする

...等々、どれだけExcelに近づけられるか的なお題が出ております。が、とりあえず時間があるときようにペンディングしておきたいと思います。

なお、総まとめ的に一つのアプリケーションを作ってみてところどころボロボロと忘れているので再復習しないとなぁ…と(´・ω・`)が、がんばりまっす

これにてコップ本写経特集は終了です

…と、半年以上続けてきたScalaコップ本の写経大会は終了です。よく続いたなぁ…と感慨にふけるところもあり、もっと早く終わりたかったところもあり…まあ、年内中に終わってなによりです

んで、次回からの(希望的観測のうえでの)予定

せっかくScalaにも(写経的な意味で書き)慣れてきたので、ぼちぼち頭をひねりながらコードを書いてみようかと&個人的にPythonコードを書く機会も増えてきているので両方の勉強が出来ればいいなぁ

…ということで、サンプルがPythonで書かれている集合知プログラミングをScalaでやってみよう、という無謀なコトをやってみたいと思います...まあ、やれるかどうかは年末年始の気分次第ということで

集合知プログラミング

ま、頑張れたら頑張ります(´・ω・`)というユルイ感じでやっていきます