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


Scalaコップ本の33章の続きをやっていきますよ(`・ω・´)33章はコップ本総まとめのスプレッドシートアプリケーションの構築です。

前回は基本的なGUI部分の実装を行ったので、今回はこいつに数式関係処理を追加していきますよ(`・ω・´)

数式

スプレッドシートアプリケーションは各セルに値と数式という2種類のデータを格納できるので、その対応をしていきますデス。まずはセルに格納される数式の種類として次の3種類を規定してやります(`・ω・´)

  • 数値
    • 1.22、-3、 0
  • テキストラベル
    • "Annual sales"、"不賛成の数"、"total"
  • セルの内容から新しい値を計算する数式
    • "= add(A1, B2)"、"sum(mul(2, A2), C1:D16)"

これら3つのセル要素に対応するために以下のようなルールを適用することになりますデス

  • 全ての算術式は引数リストに対する何らかの関数の適用
  • 関数名は二項加算のaddや任意の個数の被演算子を合計するsumなどの識別子
  • 関数の引数は数値、セル参照、セルの範囲の参照(例 C1:D16)、他の関数適用のどれか

また、このアプリケーションではミックスイン合成によって独自関数をインストール可能な構成を取るようにするみたいです。

それでは上記処理を実現する第一歩として、数式をケースクラスとして定義していくところからはじめたいと思いマス

package org.test.scells
// 関数トレイトの定義
trait Formla
//// 以下はFormulaトレイトの子として定義
// A3等のセル座標を定義
case class Coord(row:Int, column:Int) extends Formula {
  override def toString = ('A' + column).toChar.toString + row
}
// A3:B17等のセル範囲を定義
case class Range(c1:Coord, c2:Coord)extends Formula {
  override def toString = c1.toString + ":" + c2.toString 
}
// 3.1415等の数値(浮動小数点)を定義
case class Number(value:Double)extends Formula {
  override def toString = value.toString
}
// "Total"等のテキストラベルを定義
case class Textual(value:String)extends Formula {
  override def toString = value
}
// sum(A1, A2)等の関数適用
case class Application(function:String,
    arguments:List[Formula])extends Formula {
  override def toString = function + arguments.mkString("(", ",", ")")
}
// 空セルの定義
object Empty extends Textual("")

それでは上記のような定義を利用してスプレッドシートアプリケーションに数式機能を組み込んでいきますよ(`・ω・´)

数式の構文解析

それでは数式組み込みの第2歩目として数式の構文解析をやっていきますデス。具体的には31章で取り扱ったパーサー・コンビネータフレームワークのRegexParsersクラスを使って構文解析部分を組み立てて行きますデス

package org.test.scells
// パーサー・コンビネータ・フレームワークを利用しマス
import scala.util.parsing.combinator._
// RegexParsersを拡張して構文解析します
object FormulaParsers extends RegexParsers {
  
  // 識別子用補助パーサーです
  // 識別子は先頭が英字 or _ で定義します
  def ident:Parser[String] = """[a-zA-Z_]\w*""".r
  
  // 10進値のための補助パーサーです
  // \dと小数点オプションの組み合わせで数値(整数、小数)を表現シマス(´・ω・`)
  def decimal:Parser[String] = """-?\d+(\.\d*)?""".r
  
  // 座標を認識するパーサーです
  def cell:Parser[Coord] = 
    // 英字→数字の形式を認識します
    """[A-Za-z]\d+""".r ^^ { s=>
      // 認識したら英字の部分を列、数字の部分を行に分解
      // ... これだと列が26しか作れない…英字を2つ重ねる形式だといいけど
      //    方法は考えてみてYO!(by コップ本)だそうです、後回しますが(´・ω・`)  
      val column = s.charAt(0) - 'A'
      val row = s.substring(1).toInt
      // 分割した行と列から座標を設定します 
      Coord(row, column)
    }
    
  // 座標範囲を認識するパーサーです
  def range:Parser[Range] = 
    // 2つのセルが:を間に挟んだ形式を範囲として認識します
    cell~":"~cell ^^ {
      // 認識したら範囲として設定します
      case c1~":"~c2 => Range(c1, c2)
    }
      
  // 数値を認識するパーサーです
  def number: Parser[Number] =
    // 10進値がきたらDoubleに変換してNumber化シマス
    decimal ^^ (d => Number(d.toDouble))
        
  // 関数適用を認識するパーサーです
  def application:Parser[Application] = 
    // 識別子(引数)という関数形式を認識します
    ident~"("~repsep(expr, ",")~")" ^^ {
      // Applicationの実行結果を返します
      case f~"("~ps~")" => Application(f, ps)
    } 
        
  // 上の関数適用なんかで利用する引数用パーサーです
  //// 範囲をトップレベルでも使えるようにしているので
  //// 若干簡易的な定義みたいです
  //// 理想的にはここでのみ範囲を定義するのが良いとか(´・ω・`)
  def expr:Parser[Formula] = 
    range | cell | number | application
        
  // テキストラベル用のパーサーです
  def textual:Parser[Textual] = 
    """[^=].*""".r ^^ Textual

  // セルに対する全ての有効な入力を認識するパーサーです
   def formula:Parser[Formula] =
    // 有効なのは数値、テキストラベル、=付き数式デス 
    number | textual | "="~>expr
    
  // 上記文法を使って入力文字列を数式として解釈しますデス
  def parse(input:String):Formula =
   // 入力文字列をパースします
    parseAll(formula, input) match {
      // 成功の場合は変換結果を返します
      case Success(e, _) => e
      // 失敗した場合はエラーメッセージを返します
      case f:NoSuccess => Textual("[" + f.msg + "]")
    }
}

ふぅ、長くなったけども構文解析用の定義はこんな感じですな(´・ω・`)

スプレッドシートへの組み込み

定義した構文解析スプレッドシートに組み込むために、前回定義したModelクラスのCellケースクラスを次のように拡張しますデス

package org.test.scells
class Model(val height:Int, val width:Int){
  // 構文解析を行えるようにformulaフィールドを追加します
  case class Cell(row:Int, column:Int){
    var formula:Formula = Empty
    // セルの数式を表示することで
    // 構文解析の結果をチェックできるようにしますデス
    override def toString = formula.toString
  }
  val cells = new Array[Array[Cell]](height, width)
  for (i <- 0 until height; j <- 0 until width)
    cells(i)(j) = new Cell(i, j)
}

また、SpreadSheetクラスにセルへの入力イベントを組み込んでやります。セルへの入力イベントはscala.swing.eventパッケージのTableUpdatedクラスによって定義してやりますデス

// 変更されたテーブル、行セット、列を受け取ります
TableUpdated(table, rows, column)

どうやらTableUpdatedでは同じ列の連続した行であれば同時に値をうけとれるみたいです。なのでrowsはRange[Int]で渡ってくるみたいデス

それではTableUpdatedでの入力イベントの通知とイベント発生後のセルのパースを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)
          // 該当行についてFormula構文解析結果に基づいて更新されます
          cells(row)(column).formula = 
            FormulaParsers.parse(userData(row, column))
    }     
  } 
  val rowHeader = new ListView((0 until height) map (_.toString)){
    fixedCellWidth = 30
    fixedCellHeight = table.rowHeight
  }
  viewportView = table
  rowHeaderView = rowHeader
}

コンパイルして実行するとGUIアプリケーションがたちあがりますデス

$ scalac *.scala
$ scala org.test.scells.Main

うん、無事にセルに対して数値、文字列、数式を入力できるようになりました(`・ω・´)なお、異なった数式記法(例 =add(1, X)みたいにセル指定していない物)を使うと表示モードのセルにエラーが出力されますね(´・ω・`)

数式の評価

これまでの定義でセルへの数式入力が行えるようになったのですが、実際の評価が行われないので対応させてみますよ(`・ω・´)具体的には数式をパラメータとしてセルの計算結果をDoubleで返すようなコンポーネントを追加するような感じですかね。

今回このコンポーネントを、Evaluatorトレイト内のevaluateメソッドとして実装してやることにしますデス。evaluateメソッドはModelクラスのcellsフィールドにアクセスして数式内で参照されているセルの現在の状態を調査しますデス。

EvaluatorとModelの相互依存関係

その一方でModelクラスはevaluateメソッドを呼び出して各セルの評価を行うので、Modelとevaluateは相互依存関係にあるといえるみたいです。なので27章で扱ったように自分型を利用してModelとevaluateの相互依存関係を次のように表現してやります。

package org.test.scells
// ModelではEvaluateを継承しますが
// Evaluateは自分型を利用してModelを参照します
trait Evaluator { this:Model =>...

これにより、Evaluatorでもthis型がModelのインスタンスとして扱えるようになるので相互関係を表現出来るみたいです。

Evaluatorを実装します

それではEvaluatorトレイトを実装していきますデス。具体的にはevaluateメソッドとその部品を用意していく形ですね

package org.test.scells
// トレイトとして定義します
trait Evaluator {
  // 自分型を定義します
  this:Model =>
  // 評価用のメソッドです(`・ω・´)
  def evaluate(e:Formula):Double = try {
    e match {
      // セル情報が渡ってきたら該当セル内の値を表示します
      case Coord(row, column) =>
        cells(row)(column).value
      // 数値が渡ってきたら数字を表示します
      case Number(v) =>
        v
      // 文字列が渡ってきたら0として扱います
      case Textual(_) =>
        0
      // 数式が渡ってきたら処理を開始シマス(`・ω・´)
      case Application(function, arguments) =>
        val argvals = arguments flatMap evalList
        // 処理結果を返します
        // operations表は外部で定義します
        operations(function)(argvals)
    }
  // (簡易化のため)なんらかのエラーが発生したらMath.NaN_DOUBLEを返します
  // なお、「浮動小数点での値なし」に関するIEEEの規格みたいです
  }catch{
    case ex:Exception => Math.NaN_DOUBLE
  }
  
  //// 関数名から関数オブジェクトを引き出すマップを定義します
  // 演算タイプを定義しますです
  type Op = List[Double] => Double
  // 関数名と演算タイプをマッチするマップです
  val operations = new collection.mutable.HashMap[String, Op]
  
  //// トップレベル関数の引数などに渡される範囲等のセル情報を展開します
  // 例えば(A1:A3, C1)を(A1, A2, A3, C1)に展開するような処理を行います
  // 処理中ではflatMapを使って各要素を展開処理シマス
  private def evalList(e:Formula):List[Double] = e match {
    // 範囲に該当する値が渡ってきたらreferencesを使って展開し
    // 返ってきた複数要素(リスト)の各値(セル情報)を返します
    case Range(_, _) => references(e) map (_.value)
    // それ以外の場合はそのまま値を評価して返します
    case _ => List(evaluate(e))
  }
  // 実際の範囲展開の処理です
  // 展開したセル内の情報をさらに処理してやります
  def references(e:Formula):List[Cell] = e match {
    // 対象のセル内情報がセル座標の場合はセル情報を返します
    case Coord(row, column) =>
      List(cells(row)(column))
   // 対象のセル内情報が範囲だった場合はリストに展開して返します
    case Range(Coord(r1, c1), Coord(r2, c2)) =>
      for(row <- (r1 to r2).toList; column <- c1 to c2)
      yield cells(row)(column)
    // 対象のセル内情報が関数適用だった場合は
    // 個々の引数式が参照するセルをflatMapで1つのリストに連結して返します
    // 各要素を再帰的に処理しますね
    case Application(function, arguments) =>
      arguments flatMap references
    // ソレ以外の値(TextualやNumber)が渡ってきた場合は空リストを返します
    case _ =>
      List()
  }
}

とりあえず評価処理部分はこんな感じですかね(´・ω・`)フゥー...

演算ライブラリー

上記Evaluatorクラスでは評価の仕組みを提供しただけで、実際の演算処理はおこなわないデス。具体的にはoperationsが空になっているのでどこかで追加してやる必要がありマス…ということで演算関係を外出ししたArithmeticトレイトとして算術演算を定義してやりますデス

package org.test.scells
// 演算用ライブラリをトレイトとして定義します
trait Arithmetic {
  // 自分型の定義デス
  this:Evaluator =>
  // 各種演算を定義します
  operations += (
    // 和
    "add"   -> { case List(x, y) => x + y },
    // 差
    "sub"   -> { case List(x, y) => x - y },
    // 商
    "div"   -> { case List(x, y) => x / y },
   // 積
    "mul"   -> { case List(x, y) => x * y },
    // 余
    "mod"   -> { case List(x, y) => x % y },
    //// 以下は任意の引数リストをとって連続する要素間で
    //// 二項演算を行います(Listの/:演算子による左畳込み演算)
    // 合計
    "sum"   -> { xs => (0.0 /: xs)(_ + _)},
    // 積算
    "prod"   -> { xs => (1.0 /: xs)(_ * _)}
  )
}  

上記は自分型としてEvaluatorをしているので、Evaluatorの空っぽのoperationsに追加する形式になります。また、上記パターンにマッチしない(引数の数がおかしい等の)場合は遠慮無くMatchErrorをぶん投げてEvaluator.evaluateメソッド内のトライキャッチで処理させますデス。

スプレッドシートアプリケーションへの統合

仕上げとしてModelクラスにArithmeticトレイトをミックスインすることでスプレッドシートアプリケーションへの演算ライブラリの統合を行いマス(`・ω・´)

package org.test.scells
// Evaluator(相互参照) と Arithmetic を継承します
class Model(val height:Int, val width:Int)
  extends Evaluator with Arithmetic {
  case class Cell(row:Int, column:Int){
    var formula:Formula = Empty
    // 評価結果をvalueとして定義します
    def value = evaluate(formula)
    // 表示方法を変更します
    override def toString = formula match {
      // テキストラベルの場合はそのまま表示します
      case Textual(s) => s
      // ソレ以外の場合は評価結果を表示します
      case _ => value.toString
    }
  }
  val cells = new Array[Array[Cell]](height, width)
  for (i <- 0 until height; j <- 0 until width)
    cells(i)(j) = new Cell(i, j)
}

以上で組み込みは終了なのでコンパイルして実行してみますよ(`・ω・´)

$ scalac *.scala
$ scala org.test.scells.Main

うん、とりあえず数式の評価的動作はしたみたいです(`・ω・´)とりあえずOKですかね

いじょー

とりあえず今回はココマデです。次回で残り全部やりたいなぁ…ということで次回は値の変更の通知からやりますー