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
+-----------------------------------
|                                   
| +-------------------------------+ 
| |                               | 
| | +---------------------------+ | 
| | |                           | | 
| | | +-----------------------+ | | 
| | | |                       | | | 
| | | | +-------------------+ | | | 
| | | | |                   | | | | 
| | | | | +---------------+ | | | | 
| | | | | |               | | | | | 
| | | | | | +-----------+ | | | | | 
| | | | | | |           | | | | | | 
| | | | | | | +-------+ | | | | | | 
| | | | | | | |       | | | | | | | 
| | | | | | | | +---+ | | | | | | | 
| | | | | | | | |   | | | | | | | | 
| | | | | | | | | + | | | | | | | | 
| | | | | | | | +-+ | | | | | | | | 
| | | | | | | |     | | | | | | | | 
| | | | | | | +-----+ | | | | | | | 
| | | | | | |         | | | | | | | 
| | | | | | +---------+ | | | | | | 
| | | | | |             | | | | | | 
| | | | | +-------------+ | | | | | 
| | | | |                 | | | | | 
| | | | +-----------------+ | | | | 
| | | |                     | | | | 
| | | +---------------------+ | | | 
| | |                         | | | 
| | +-------------------------+ | | 
| |                             | | 
| +-----------------------------+ | 
|                                 | 
+---------------------------------+ 

おお、出来ましたなー

いジョー

オブジェクト指向的な日々をお送りしましたが、ようやく10章終わりです(´・ω・`)

次は11章のScala的階層構造に進みますが…まだまだ先は長いなぁ…が、がんばります