前回取り上げたNamed and Default Argumentsですが、DSLにも大きな影響があります。
ScalaのDSLというと、ScalaTestなどが提供している以下のようなDSLを思い浮かべます。
class StackSpec extends FlatSpec with ShouldMatchers {
"A Stack" should "pop values in last-in-first-out order" in {
val stack = new Stack[Int]
stack.push(1)
stack.push(2)
stack.pop() should equal (2)
stack.pop() should equal (1)
}
it should "throw NoSuchElementException if an empty stack is popped" in {
val emptyStack = new Stack[String]
evaluating { emptyStack.pop() } should produce [NoSuchElementException]
}
}
こういった華麗なDSLの問題は、開発コストが結構かかるという点です。また、Scalaの物理的な文法に沿いながらも、独自の文法を編み出すということでもあるので、利用者側の学習コストも馬鹿になりません。
ScalaTest級の大物フレームワークの場合は、こういったところに力を入れても得るところが大きいですが、ちょっとしたマイ・ローカル・プログラムではなかなかこういう所にコストを掛けるのも大変です。
そこで、ボクが最近愛用しているのが、地味にcase classを使う方法です。
たとえば、こういうcase classを定義します。
case class Config(
name: String,
version: String = "1.0",
drivers: Seq[Driver] = Nil)
case class Driver(
name: String,
url: String,
params: Map[String, String] = Map.empty)
Driver("google", "http://www.google.com")))
これを、こういう感じでDSLに使います。
Config("foo", drivers = List(
Driver("yahoo", "http://www.yahoo.com"),
Driver("google", "http://www.google.com")))
Named and Default Argumentsの機能を活用することで、不要なパラメタ設定を減らすことができるのが魅力的です。2.8以前はこういうことができなかったので、case classでDSLを作ることのメリットが限定的だったのですが、最新仕様では状況がかわっているというわけです。
細かいですが、以下のようにcase classをtoStringで文字列化した時に、データの内容が分かりやすく整形されるのは、デバッグ時にうれしい機能です。
scala> Config("foo", drivers = List(
| Driver("yahoo", "http://www.yahoo.com"),
| Driver("google", "http://www.google.com")))
res2: Config = Config(foo,1.0,List(Driver(yahoo,http://www.yahoo.com,Map()), Driver(google,http://www.google.com,Map())))
このDSLは、case classの特徴を引き継いでおり代数的データ型と永続データ構造の性質を合わせ持つ不変オブジェクトでもあるので、内部データ構造としてもそのまま使うことができます。前回紹介したcopy constructorも強い味方です。
華麗なDSLの場合、内部処理用のデータ構造に情報を転記しなければならないことになりがちなので、その作業が不要になるのはかなり大きなメリットです。