sbt のタスク定義が大変なので DSL を作ってみた

( [Scala Advent Calendar 2013](http://qiita.com/advent-calendar/2013/scala) の 20 日目です ) 

# sbt のタスクを定義する

こんなコードを考えてみます。

```scala:list1
  def unreadableSettings = {
    val hoge1 = TaskKey[Unit]("hoge1") in Compile
    val hoge2 = TaskKey[Unit](":hoge2") in Compile
    val hoge3 = TaskKey[Unit](":hoge3") in Compile

    val fuga1 = TaskKey[Unit](":fuga1") in Compile
    val fuga2 = TaskKey[Unit](":fuga2") in Compile
    Seq(
      hoge1 := println("hoge1!"),
      hoge2 := println("hoge2!"),
      hoge3 := println("hoge3!"),

      fuga1 := println("fuga1!"),
      fuga2 := println("fuga2!"),

      fuga1 <<= fuga1 dependsOn fuga2,
      fuga1 <<= fuga1 andFinally println("fuga1 is over!"),

      hoge1 <<= hoge1 dependsOn fuga1,
      hoge1 <<= hoge1 dependsOn hoge2,
      hoge1 <<= hoge1 dependsOn hoge3,
      hoge1 <<= hoge1 andFinally println("hoge1 is over!")
    )
  }
```

やや恣意的な例ですがそこは気にしてはいけません。

## とりあえず実行だ

```:list2
> hoge1
hoge3!
hoge2!
fuga2!
fuga1!
fuga1 is over!
hoge1!
hoge1 is over!
```

ふむり…？

# 解読する

list1 を読み下してみると下記のようになります。

* fuga1 の前に fuga2 を実行
* fuga1 が終わったら "fuga1 is over!" を出力
* hoge1 の前に fuga1 を実行
* その前に hoge2 を実行
* その前に hoge3 を実行
* hoge1 が終わったら "hoge1 is over!" を出力

なるほどわからん

# こんなの読むのも書くのも無理

なので DSL を作ってみました。

```scala:list2
  def readableSettings = new TaskSettings{
    def foo =
      define("foo1") as (
        call to {
          println("foo3!")
          println("foo2!")
        },
        bind (
          call to println("bar2!"),
          call to println("bar1!")
        ) ensure {
          println("bar1 is over!")
        },
        call to println("foo1!")
      ) ensure {
        println("foo1 is over!")
      }
  }
```

これでやっと安心して通常の逐次処理のように読めるようになりました。
あとはいつもどおり ProjectSetting に加えてしまえば準備完了です。

```scala
settings = Project.defaultSettings ++ readableSettings.foo.toSeq
```

foo1 というタスクの出力を確認してみます。

```
> foo1
foo3!
foo2!
bar2!
bar1!
bar1 is over!
foo1!
foo1 is over!
```

計画通り（ﾄﾞﾔｧ

# まとめ

* sbt では複数のタスクから依存関係のある新たなタスクをつくるのがとても大変 ( 並列実行は簡単 )
* DSL で軽くラップすると素直なコードが書けるようになる

# おまけ

## 開発中のあるある要件

として「この一連の bar 出力は単体で実行できるタスクにしたい」みたいなケースがよくありますが、この DSL ではそれも考慮されています。

bind の部分を下記のように置き換えるだけで

```scala
define("bar1") as (
```

その時点で bar1 というタスクが定義されます。

```
> bar1
bar2!
bar1!
bar1 is over!
```

シンプルですね。
 ( 新たに外に TaskKey を作ってそれを登録して dependsOn でつなげる手間はもう必要ありません )

## 既存の TaskKey を組み込む

こともできます。
これは "bar2!" を出力した直後に clean タスクを実行する例です。

```scala
        bind (
          call to println("bar2!"),
          task of clean,
          call to println("bar1!")
        ) ensure {
          println("bar1 is over!")
        },
```

# DSL の中身

```scala:list3
class TaskSettings extends SettingsAlias{

  def bind(label: String)(binders: Binder*): Multiple = {
    val key = TaskKey[Unit](label) in Compile
    val settings = binders.reverse.map(_ dependenciesFrom key).flatten
    new Multiple(key, settings)
  }
  def bind(binders: Binder*): Multiple = bind(createLabel)(binders:_*)

  def execute[A](f: => A) = {
    val key = TaskKey[Unit](createLabel) in Compile
    new Procedure(key, () => f)
  }
  def register[A](taskKey: TaskKey[A]) = new KeyStore(taskKey)

  import java.util.UUID
  private def createLabel = "ts:dummy:" + UUID.randomUUID

  private type Settings = Seq[Project.Setting[Task[Unit]]]

  trait Binder {
    def taskKey: TaskKey[_]
    def dependenciesFrom(key: TaskKey[Unit]): Settings
    def connectFrom(key: TaskKey[Unit]) = Seq(key <<= key dependsOn taskKey)
  }
  class KeyStore[A](val taskKey: TaskKey[A]) extends Binder {
    def dependenciesFrom(key: TaskKey[Unit]) = connectFrom(key)
  }
  class Procedure(val taskKey: TaskKey[Unit], f: () => Unit) extends Binder {
    def dependenciesFrom(key: TaskKey[Unit]) = Seq(taskKey := f()) ++ connectFrom(key)
  }
  class Multiple(val taskKey: TaskKey[Unit], settings: Settings) extends Binder{
    def ensure[A](f: => A) = {
      new Multiple(taskKey, settings ++ Seq(taskKey <<= taskKey andFinally f))
    }
    def dependenciesFrom(key: TaskKey[Unit]) = toSeq ++ connectFrom(key)
    def toSeq = Seq(taskKey := {}) ++ settings
  }
}

trait SettingsAlias { self: TaskSettings =>
  def define(label: String) = new {
    def as (binders: Binder*) = bind(label)(binders:_*)
  }
  def call = new {
    def to (f: => Unit) = execute(f)
  }
  def task = new {
    def of (taskKey: TaskKey[_]) = register(taskKey)
  }
}
```