LL脳がscalaの勉強を始めたよ その33
Scalaコップ本の10章の残りをやってしまいますよー
前回までに作成した2Dレイアウトライブラリーは次のような感じですねー
// コンパニオンオブジェクトをインポート 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 = { elem(this.contents ++ that.contents) } // 並べメソッド def beside(that: Element):Element = { elem( for ( (line1, line2) <- this.contents zip that.contents ) yield line1 + line2 ) } // 表示するメソッド 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) } }
今回はこいつの仕上げをしたいと思いますよー
とりあえす使ってみますかね
ここまで頑張って作ったライブラリをとりあえず動かしてみましょうか
手順的にはコンパイルして読み込んで使うだけですが…まあやってみましょう
// ライブラリコードを保存したファイルElement.scalaをコンパイルしますよ % scalac Element.scala // テスト用に対話コンソールを呼び出します % scala // ライブラリを呼び出します scala> import Element.elem import Element.elem // 配列からElementオブジェクトを生成してみますよ scala> val e1 = elem(Array("hello", "world", "wide")) e1: Element = hello world wide // 文字列からElementオブジェクトを生成してみますよ scala> val e2 = elem("scala world") e2: Element = scala world // フォーマット文から矩形のElementオブジェクトを生成してみますよ scala> val e3 = elem('!', 7, 5) e3: Element = !!!!!!! !!!!!!! !!!!!!! !!!!!!! !!!!!!! // 上に重ねますねー scala> e1 above e2 res0: Element = hello world wide scala world // 横に並べますねー scala> e1 beside e2 res2: Element = helloscala
とりあえずこんな感じに動作しております(`・ω・´)
高さを調整するheighten、幅を調整するwiden
前回まで作成したElementでのabove、besideの各メソッドはソレゾレ、幅・高さが同じ要素でないと正常に評価できないメソッドだったのでコレを修正していきますー
aboveとbesideのダメな点
現状のaboveメソッドでは上の実行結果にあるとおり幅の調整を行わないので、次のようにaboveの演算順序によってはwidthの値が異なってしまうという交換法則ブッチギリの世界になっております。
// e1とe2の重ねあわせ結果の幅 scala> (e1 above e2).width res10: Int = 5 // 逆に重ねあわた結果の幅 scala> (e2 above e1).width res11: Int = 11 // 数学的にそんな世界は沢山あるから気にすんな!って言われればそれまでデスケド
また、besideメソッドでは上の例にあるとおり、高さが余った方の行が捨てられてしまうという問題が発生してしまってます 。
…そんなわけでソレゾレを修正するwiden, heightenメソッドを作りますよ
widenメソッド
重ねあわせるaboveメソッド時に2つのElementオブジェクトの幅を調整するwidenメソッドを実装しますよー
具体的には2つのオブジェクトのうち幅の小さい方を空白埋めしてやる形で同じ幅に調整したいので、比較幅調整用のメソッドを次のように実装します。
// ターゲットオブジェクトの幅を引数に取りますよ def widen(w:Int):Element = // ターゲットの幅より自身の幅が大きければ自分自身をそのまま返します if(w <= width) this // ターゲット幅の方が大きい場合は両サイドに空白を追加して調整します else { // 空白用Elementオブジェクトを生成 val left = elem(' ', (w -width) / 2 ,height) val right = elem(' ', w -width - left.width ,height) // besideメソッドを利用して横に並べますよ left beside this beside right }
実際に使うときは2つのElementオブジェクト(例:A, B)の両側での比較調整で使う感じになりますかねー(A widen B.widthとB widen A.widthの2つの処理をします)、詳しい適用方法はあとの方でー
heigthenメソッド
高さ調整もwidenと同様に小さい方に空白をくっつけて同じ高さにするheightenメソッドで実現しますよー
実装内容もほとんどwidenとおなじですなー
// ターゲットオブジェクトの高さを引数に取りますよ def heighten(h:Int):Element = // ターゲットの高さより自身の高さが大きければ自分自身をそのまま返します if(h <= height) this // ターゲット高さの方が大きい場合は上下に空白を追加して調整します else { // 空白用Elementオブジェクトを生成 val top = elem(' ', width / 2 ,(h - height) / 2) val bottom = elem(' ', width ,h - height - top.height) // aboveメソッドを利用して縦に並べますよ top above this above bottom }
Elementへ組み込みますー
widenとheightenをElementクラスに組み込んで、above・besideメソッドに適用しますよー
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) <- this.contents zip that.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") }
修正結果を試しますかね
んじゃ、実行しますねー
// コンパイルします %scalac Element.scala // 対話コンソール呼び出します % scala // インポートします scala> import Element.elem import Element.elem // Elementオブジェクトを作成しますよ scala> val e1 = elem(Array("hello", "world", "wide")) e1: Element = hello world wide scala> val e2 = elem("scala world") e2: Element = scala world // 重ねます scala> e1 above e2 res0: Element = hello world wide scala world // 逆に重ねます scala> e2 above e1 res2: Element = scala world hello world wide // どっちから処理しても同じ結果デス scala> (e1 above e2).width res3: Int = 11 scala> (e2 above e1).width res4: Int = 11 // 並べますよ scala> e1 beside e2 res5: Element = hello worldscala world wide // 逆に並べますよ scala> e2 beside e1 res6: Element = hello scala worldworld wide // どちらから処理しても同じ高さが出ますねー scala> (e1 beside e2).height res7: Int = 3 scala> (e2 beside e1).height res8: Int = 3
これでライブラリーが出来ましたねー
レイアウト要素すべてを組み合わせて利用するアプリケーション
作成したライブラリを実際に使用するアプリケーションを実装してみますよー今回作成するサンプルは螺旋を描画するものです。
動作的にはコマンド引数から螺旋の辺数をとるスタンドアロンオブジェクトの形式にしてみますね
import Element.elem object Spiral { // 空白を定義 val space = elem(" ") // 角は+で表現 val corner = elem("+") // 引数として辺の数と位置(4辺のどれか)を表す数値 //(上:0、右:1、下:2、左:3)をとる再帰関数 def spiral(nEdges:Int, direction: Int):Element = { // 中心部になったら + だけを返す if(nEdges == 1) elem("+") // 中心部以外の場合は螺旋要素を生成 else{ // 内側の螺旋Elementを再帰的に生成して取得 val sp = spiral(nEdges -1, (direction + 3) % 4) /*** ココカラ螺旋要素の生成 ***/ // 縦バーを表現するElementを生成 def verticalBar = elem('|', 1, sp.height) // 横バーを表現するElementを生成 def horizontalBar = elem('-', sp.width, 1) // 上辺の場合の描画 if(direction == 0) // 上辺として水平線とコーナー記号を追加し // 内側螺旋の右部分に空白列を追加したものと結合 (corner beside horizontalBar) above (sp beside space) // 右辺の場合の描画 else if(direction == 1) // 右辺として縦線とコーナー記号を追加し // 内側螺旋の下部分に空白列を追加したものと結合 (sp above space) beside (corner above verticalBar) // 下辺の場合の描画 else if(direction == 2) // 下辺として水平線とコーナー記号を追加し // 内側螺旋の左部分に空白列を追加したものと結合 (space beside sp) above (horizontalBar beside corner) // 左辺の場合の描画 else // 左辺として縦線とコーナー記号を追加し // 内側螺旋の上部分に空白列を追加したものと結合 (verticalBar above corner) beside (space above sp) } } // メイン処理 def main(args:Array[String]) { val nSides = args(0).toInt println(spiral(nSides, 0)) } }
実行結果ですよー
スタンドアロンオブジェクトなのでコンパイルして実行しますよー
// コンパイルします % scalac Spiral.scala // 辺が6つのスパイラルです % scala Spiral 6 +----- | | +-+ | + | | | +---+ // 辺が36の大スパイラルです % scala Spiral 36 +----------------------------------- | | +-------------------------------+ | | | | | +---------------------------+ | | | | | | | | | +-----------------------+ | | | | | | | | | | | | | +-------------------+ | | | | | | | | | | | | | | | | | +---------------+ | | | | | | | | | | | | | | | | | | | | | +-----------+ | | | | | | | | | | | | | | | | | | | | | | | | | +-------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +---+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | + | | | | | | | | | | | | | | | | +-+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +-----+ | | | | | | | | | | | | | | | | | | | | | | | | | | | +---------+ | | | | | | | | | | | | | | | | | | | | | | | +-------------+ | | | | | | | | | | | | | | | | | | | +-----------------+ | | | | | | | | | | | | | | | +---------------------+ | | | | | | | | | | | +-------------------------+ | | | | | | | +-----------------------------+ | | | +---------------------------------+
おお、出来ましたなー