( Scala Advent Calendar 2013 の 20 日目です )
sbt のタスクを定義する
こんなコードを考えてみます。
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 を作ってみました。
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 に加えてしまえば準備完了です。
settings = Project.defaultSettings ++ readableSettings.foo.toSeq
foo1 というタスクの出力を確認してみます。
> foo1
foo3!
foo2!
bar2!
bar1!
bar1 is over!
foo1!
foo1 is over!
計画通り(ドヤァ
まとめ
- sbt では複数のタスクから依存関係のある新たなタスクをつくるのがとても大変 ( 並列実行は簡単 )
- DSL で軽くラップすると素直なコードが書けるようになる
おまけ
開発中のあるある要件
として「この一連の bar 出力は単体で実行できるタスクにしたい」みたいなケースがよくありますが、この DSL ではそれも考慮されています。
bind の部分を下記のように置き換えるだけで
define("bar1") as (
その時点で bar1 というタスクが定義されます。
> bar1
bar2!
bar1!
bar1 is over!
シンプルですね。
( 新たに外に TaskKey を作ってそれを登録して dependsOn でつなげる手間はもう必要ありません )
既存の TaskKey を組み込む
こともできます。
これは "bar2!" を出力した直後に clean タスクを実行する例です。
bind (
call to println("bar2!"),
task of clean,
call to println("bar1!")
) ensure {
println("bar1 is over!")
},
DSL の中身
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)
}
}