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


Scalaコップ本の15章の最後をやってしまいますよー、15章のラストはパターンマッチを使って算術式を見やすい形式で出力するライブラリを作成します。

算術式は前回までのクラスをそのまま、表示生成は第10章で作成したレイアウトライブラリを使いマスね。今回で50回目というキリのいい回です…特にすることもないので淡々とやっていきましょー

レイアウトライブラリ

10章で作成した2Dレイアウトライブラリはこんな感じでした

いちおうコードも載せておきますかねー

package com.test.elem
// コンパニオンオブジェクトをインポート
import Element.elem
// クラス定義

abstract class Element {
  def contents:Array[String]
  def height:Int = contents.length
  def width:Int = if(height == 0) 0 else contents(0).length
  // 重ねメソッド
  def above(that: Element): Element =  {
    // widenメソッドで幅調整します
    val this1 = this widen that.width
    val that1 = that widen this.width
    // 調整結果を利用して重ねあわせますよ 
    elem(this1.contents ++ that1.contents)
  }
  // 並べメソッド
  def beside(that: Element):Element = {
    // heightenメソッドで高さ調整します
    val this1 = this heighten that.height
    val that1 = that heighten this.height
    // 調整結果を利用して重ねあわせますよ
    elem(
      for (
        (line1, line2) <- this1.contents zip that1.contents
      ) yield line1 + line2
    )
  } 
  // 幅調整メソッド
  def widen(w:Int):Element =
  if(w <= width) this
  else {
    val left = elem(' ', (w -width) / 2 ,height)
    val right = elem(' ', w -width - left.width ,height)
    left beside this beside right
  }
  // 高さ調整メソッド
  def heighten(h:Int):Element =
  if(h <= height) this
  else {
    val top = elem(' ', width / 2 ,(h - height) / 2)
    val bottom = elem(' ', width ,h - height - top.height)
    top above this above bottom
  }
  override def toString = contents.mkString("\n")
}

object Element {
  // 配列を基にElementを構築
  private class ArrayElement(val contents:Array[String]) extends Element
  // 文字列を基にElementを構築
  private class LineElement(s:String) extends Element{
    val contents = Array(s)
    override def width = s.length
    override def height = 1
  }
  // 特定範囲を指定文字で埋め尽くしたElementを構築
  private class UniformElement(
    ch:Char,
    override val width:Int,
    override val height:Int
   ) extends Element {
    private val line = ch.toString * width
    def contents = Array.make(height, line)
  }
  // ArrayElementの呼び出し用
  def elem(contents:Array[String]):Element = {
    new ArrayElement(contents)
  }
  // UniformElementの呼び出し用
  def elem(char:Char, width:Int, height:Int):Element = {
    new UniformElement(char, width, height)
  }
  // LineElementの呼び出し用
  def elem(line:String):Element = {
    new LineElement(line)
  }
}

算術式レイアウト整形ライブラリ

上記の2Dレイアウトライブラリを利用して算術式を表示しますデス。例えばBinOp("+", Number(3), Var("x"))を整形するとイメージ的にはこんな感じになります。

3 + x

それとせっかく2Dレイアウトライブラリを使うのでなので、除算は分数表記してみましょう。例えばBinOp("/", Number(3), Var("x"))はこんな感じですね

3
-
x

あとは演算順序がわかるように優先度の弱い演算子の算術式の場合は適切に"( )"をつけた表記にするとか、そのあたりに気をつけながらコードを書いてみますねー

サンプルコード
package com.test.expr
// レイアウト要素生成パッケージをインポート
import com.test.elem.Element
import com.test.elem.Element.elem

// 抽象基底クラス
abstract class Expr
//// 以下はケースクラスの定義です
// 変数定義クラス 
case class Var(name:String) extends Expr
// 数値定義クラス
case class Number(num:Double) extends Expr
// 単項演算クラス
case class UnOp(opearator:String, arg:Expr) extends Expr
// 二項演算クラス
case class BinOp(opearator:String, left:Expr, right:Expr) extends Expr

// 算術式ライブラリをレイアウト要素に整形するクラス
class ExprFormatter {

  // 演算子の優先順序を定義(下に行くほど強め)
  private val opGroups = {
    Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-"),
      Set("*", "%"),
    )
  }

 // opGroupsを基に演算子から優先順序を取得するMapを生成  (*1
  private val precedence = {
    val assocs = {
      // opGroupを基に"演算子 -> 優先順序"の対応関係を作成
      for {
        i <- 0.until(opGroups.length)
        op <- opGroups(i)
      } yield op -> i
    }
    // 生成した対応関係をMapに格納
    Map() ++ assocs
  }
  
  // 演算子定義数を格納
  private val unaryPrecedence = opGroups.length
  // 除算時の表示制御のための変数を初期化
  private val fractionPrecedence = -1
  
  // 数式をレイアウト要素に整形する処理
  private def format(e:Expr, enclPrec:Int):Element = e match {
    
    // 変数の場合はそのままレイアウト要素に整形
    case Var(name) => elem(name)
    
    // 数式が数値の場合は数値から作ったレイアウト要素に整形
    case Number(num) => {
      // 文字列から".0"の表記を取り除いて浮動小数点表記を整えます
      def stripDot(s:String) = {
        if(s.endsWith(".0")) s.substring(0, s.length - 2) else s
      }
      // stripDotを使って整形した数値を基にレイアウト要素に
      elem(stripDot(num.toString))
    } 
    // 数式が単項演算の場合、前述の優先順序をベースにレイアウト要素に整形
    case UnOp(op, arg) => elem(op).beside(format(arg, unaryPrecedence))
    
    // 数式が二項演算の除算の場合は上下に重ねたレイアウト要素に整形
    case BinOp("/", left, right) => {
      val top = format(left, fractionPrecedence)
      val bot = format(right, fractionPrecedence)
      val line = elem('-', top.width.max(bot.width), 1)
      // 上下に重ねたレイアウト要素を一時的に生成
      val frac = top.above(line.above(bot))
      // この除算が他の除算の引数でない場合はそのままリターン
      if(enclPrec != fractionPrecedence) frac
      // この除算が他の除算の引数の場合は見やすいように整形 します(*2
      else elem(" ").beside(frac.beside(elem(" ")))
    }
    // 数式が除算以外の二項演算の場合は演算子と
    // 計算対象の各要素を並べた形式のレイアウト要素に整形
    case BinOp(op, left, right) => {
      val opPrec = precedence(op)
      // 左要素をレイアウト要素に整形
      val l = format(left, opPrec)
      // 右要素をレイアウト要素に整形
      val r = format(right, opPrec + 1)
      // 左要素、演算子、右要素を並べたレイアウト要素を仮生成
      val oper = l.beside(elem(" " + op + " ").beside(r))
      // この二項演算の演算子が
      // この数式を引数とする数式の演算子よりも優先順序が低ければ
      // () をつけて表記します。優先順位が高ければそのままです
      if (enclPrec <= opPrec) oper
      else elem("(").beside(oper.beside(elem(")")))
    }
  } 
  def format(e:Expr):Element = format(e, 0)  
}||<

*** 補足: *1

生成するMapの形式は次のようになります
>>
Map("|" -> 0, "||" -> 0. "&" -> 1, "&&" -> 1, ...)
<<
演算子を指定すれば優先度(数字が大きいほど大きい)が取得できるMapですね

*** 補足: *2

分数の分数みたいな表記を見やすくします。例えば( 3 / x ) / 4のような算術式であれば、次のようにどこがベースになるのかを見やすくします。
>>
 3
 -
 x
---
 4
<<

** 実行用アプリケーション

さて、実際のアプリケーションを書いてみますYO!

Applicationトレイトを継承して実行可能な形式にしますね
>|scala|
// 算術式整形ライブラリをインポートしマス
import com.test.expr._

// Applicationトレイトを使って処理を見やすくします
object Express extends Application {
  val f = new ExprFormatter
  
  //// 算術式を定義します
  // (1 / 2 ) * (x + 1)です 
  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)), BinOp("+", Var("x"), Number(1)))
  // (1 / 2 ) * (1.5 / x)です 
  val e2 = BinOp("*", BinOp("/", Var("x"), Number(2)), BinOp("/", Number(1.5), Var("x")))
   // ((1 / 2 ) * (x + 1))  / ((1 / 2 ) * (1.5 / x))です
  val e3 = BinOp("/", e1, e2)
  
  // 各算術式を表示しますよー
  def show(e:Expr) = println(f.format(e) + "\n\n")
  for(val e <- Array(e1, e2, e3)) show(e)
}

コンパイルして実行

// コンパイルしますよ
% scalac ExprExe.scala Expr.scala Element.scala
// なんかWarningが出てるけども…とりあえずあとで考えよう
warning: there were deprecation warnings; re-run with -deprecation for details
one warning found

// 生成されたバイナリを実行しますよ
% scala Express

// e1をレイアウト化しましたよ
1          
- * (x + 1)
2          

// e2をレイアウト化しましたよ
x   1.5
- * ---
2    x 

// e3をレイアウト化しましたよ
1          
- * (x + 1)
2          
-----------
  x   1.5  
  - * ---  
  2    x   

うん、とりあえず出来ましたねー

いじょー

15章が漸く終了です。次回は16章のリスト操作ですね…後半分くらいかな…が、頑張ります(´・ω・`)