倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Scala で Groovy のビルダーみたいなことをする

ちょっと前に「倭マンのつぶや記」で書いたエントリの焼き直し記事。

Groovy のビルダーは HTML などのツリー構造を作るのに便利なクラスですが、Groovy の言語特性をかなり使ってるので Scala ではできないかなぁ?と思ってたんですが、実際にやってみると(少なくとも要素名が決まってるような場合には)それなりに同じようなコードが簡単に書けるもよう。 少々感じが違うのは、Groovy のビルダーではビルダーオブジェクトのインスタンスを生成した後にそのインスタンスに対してメソッドを呼び出してツリー構造を生成するのに対して、ここで見ていく方法ではビルダークラスのサブクラスの定義でツリー構造を生成していきます。

この記事では、ビルダーを使ってディレクトリ構造を生成する DirectoryBuilder というクラスを作ってみます。

DirectoryBuilder の使用例

まずはここで見ていく Scala のビルダーがどういう風に使われるのかを見てみましょう。 例えばよくある簡単な sbt プロジェクト(Maven2/3 や Gradle と同じ)のディレクトリ構造を作るコードは以下のようになります:

// プロジェクトを作成したいディレクトリ
val projectHome = ...

new DirectoryBuilder {

  val baseDir = projectHome  // プロジェクトを作成したいディレクトリをセット

  file("build.sbt")
  file("README.md")
  file(".gitignore")
  dir("project") {
    file("build.properties")
  }
  dir("src") {
    dir("main") {
      dir("scala") {
        file("MyFirstApp.scala")
      }
    }
    dir("test") {
      dir("scala") {
        file("MyFirstAppSpec.scala")
      }
    }
  }
}

まぁ、使い方は見ればだいたい明らかだと思いますが(それがビルダーのメリット)、一応簡単な説明を。

  • 起点となるディレクトリ(ここではプロジェクトを作りたいディレクトリ)を baseDir フィールドにセットする
  • dir() メソッドでディレクトリを作成する
  • file() メソッドでファイルを作成する

ここでは、DirectoryBuilder クラスの無名サブクラスの定義部分でツリー構造を生成しています。

事前にルートととなるディレクトリを作るのが面倒な場合は、作成したインスタンスに対して baseDir プロパティからそのディレクトリを取得できます:

val projectHome = new DirectoryBuilder {

  val baseDir = Files.createTempDirectory(null, null)
  ...
}.baseDir  // ルート・ディレクトリを取得

「ルート・ディレクトリ」と言うとファイルシステムのルートっぽいので名前を baseDir にしてますが、まぁ rootDir にしても特に問題なかった気もします。

DirectoryBuilder の実装

ファイルやディレクトリの生成は java.nio.file.Files クラスのメソッドを使えば簡単だってのもありますが、DirectoryBuilder クラスは結構簡単に実装できるので載せておきましょう。

import java.nio.file.{Files, Path}
import java.nio.file.attribute.FileAttribute

abstract class DirectoryBuilder{

  val baseDir: Path
  private var currentDir: Path = null

  private def initCurrentDirIfNull(): Unit =
    if(this.currentDir == null)
      this.currentDir = this.baseDir

  /** 空ディレクトリを作成する */
  protected def emptyDir(name: String, attrs: FileAttribute[_]*): Path = {
    initCurrentDirIfNull()
    Files.createDirectory(this.currentDir.resolve(name), attrs:_*)
  }

  /** ディレクトリを作成する */
  protected def dir(name: String, attrs: FileAttribute[_]*)(buildDir: => Unit): Path = {
    val newDir = emptyDir(name, attrs:_*)

    val oldDir = this.currentDir
    this.currentDir = newDir
    buildDir
    this.currentDir = oldDir

    newDir
  }

  /** ファイルを作成する */
  protected def file(name: String, attrs: FileAttribute[_]*): Path = {
    initCurrentDirIfNull()
    Files.createFile(this.currentDir.resolve(name), attrs:_*)
  }
  
  /** リンクを作成する */
  protected def link(name: String, target: Path): Path = {
    initCurrentDirIfNull()
    Files.createLink(this.currentDir.resolve(name), target)
  }
  
  /** シンボリックリンクを作成する */
  protected def symbolicLink(name: String, target: Path, attrs: FileAttribute[_]*): Path = {
    initCurrentDirIfNull()
    Files.createSymbolicLink(this.currentDir.resolve(name), target, attrs:_*)
  }
}

まぁ、普通の入れ子構造を処理するコードでございます。 emptyDir() メソッドは空のディレクトリを作るためのメソッドで、以下のように使います:

emptyDir("target")
// 空のディレクトリを作るには以下のように書いても OK
dir("target"){}

まぁ、emptyDir と書くか最後の中括弧 {} を書くかという違いです。

ちなみに以前につぶや記で書いたコードから以下の部分をちょっと変えてます:

  • ファイル属性を指定してファイル、ディレクトリを作成できるようにした
  • リンク、シンボリックリンクを作成できるようにした
  • ファイルの内容は書けないようにした

ファイルの内容を書けないようにしたのは、file() メソッドの引数としてファイル属性と衝突してしまうことと、Path クラスに暗黙の型変換で機能を追加した方が高機能にできる、と理由からでございます。 例えば、Path クラスに GDK (Groovy JDK) にあるような「<<」演算子(Scala では普通のメソッドだけど)を追加してやると

val projectHome = new DirectoryBuilder {

  val baseDir = GluinoPath.createTempDirectory(prefix = "project-")

  file("build.sbt") <<
    s"""name := "${baseDir.getFileName}"
        |
        |version := "0.1-SNAPSHOT"
        |
        |libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test"
        |""".stripMargin

  file("README.md") << "Read me!"
  file(".gitignore") << "target/"
  dir("project") {
    file("build.properties") << "sbt.version=0.13.8"
  }
  dir("src") {
    dir("main") {
      dir("scala") {
        file("MyFirstApp.scala") <<
          """class MyFirstApp extends App{
            |  println("Hello World!")
            |}""".stripMargin
      }
    }
    dir("test") {
      dir("scala") {
        file("MyFirstAppSpec.scala") <<
          """import org.scalatest.{FlatSpec, Matchers}
            |
            |class MyFirstAppSpec extends FlatSpec with Matchers{
            |
            |}""".stripMargin
      }
    }
  }
}.baseDir

のように書くことができます。 「<<」演算子(メソッド)の実装も Files クラスを使えば簡単です(ただし暗黙の型変換を行わせるコードは必要ですが)。 他に withWriter() のようなローンパターン用のメソッドやエンコーディングを指定して書き込むメソッドなども作れます。

まとめ

ここではファイル属性やリンク、シンボリックリンクも使えるようにしましたが、これらの機能も抜けばもっと短いコードでディレクトリ構造を作成するコードが書けます。 ちょっと心配なのが、関数型プログラミングってナニ?みたいなコードになってる所ですが、まぁ別に Scala はオブジェクト指向を排除してるわけではないし、使えそうなときには使ってみてはいかがでしょうか。
Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

  • 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
  • 出版社/メーカー: インプレスジャパン
  • 発売日: 2011/09/27
  • メディア: 単行本(ソフトカバー)
  • 購入: 12人 クリック: 235回
  • この商品を含むブログ (46件) を見る
プログラミングGROOVY

プログラミングGROOVY

  • 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/07/06
  • メディア: 単行本(ソフトカバー)
  • 購入: 6人 クリック: 392回
  • この商品を含むブログ (155件) を見る