Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is an sbt task defined using <<= different from one defined with := that references another setting's .value?

Tags:

scala

sbt

I have the following example build.sbt that uses sbt-assembly. (My assembly.sbt and project/assembly.sbt are set up as described in the readme.)

import AssemblyKeys._

organization := "com.example"

name := "hello-sbt"

version := "1.0"

scalaVersion := "2.10.3"

val hello = taskKey[Unit]("Prints hello")

hello := println(s"hello, ${assembly.value.getName}")

val hey = taskKey[Unit]("Prints hey")

hey <<= assembly map { (asm) => println(s"hey, ${asm.getName}") }

//val hi = taskKey[Unit]("Prints hi")

//hi <<= assembly { (asm) => println(s"hi, $asm") }

Both hello and hey are functionally equivalent, and when I run either task from sbt, they run assembly first and print a message with the same filename. Is there a meaningful difference between the two? (It seems like the definition of hello is "slightly magical", since the dependency on assembly is only implied there, not explicit.)

Lastly, I'm trying to understand why hey needs the map call. Obviously it results in a different object getting passed into asm, but I'm not quite sure how to fix this type error in the definition of hi:

sbt-hello/build.sbt:21: error: type mismatch;
 found   : Unit
 required: sbt.Task[Unit]
hi <<= assembly { (asm) => println(s"hi, $asm") }
                                  ^
[error] Type error in expression

It looks like assembly here is a [sbt.TaskKey[java.io.File]][2] but I don't see a map method defined there, so I can't quite figure out what's happening in the type of hey above.

like image 357
bstpierre Avatar asked Dec 17 '13 19:12

bstpierre


1 Answers

sbt 0.12 syntax vs sbt 0.13 syntax

Is there a meaningful difference between the two?

By meaningful difference, if you mean semantic difference as in observable difference in the behavior of the compiled code, they are the same.

If you mean, any intended differences in code, it's about the style difference between sbt 0.12 syntax sbt 0.13 syntax. Conceptually, I think sbt 0.13 syntax makes it easier to learn and code since you deal with T instead of Initialize[T] directly. Using macro, sbt 0.13 expands x.value into sbt 0.12 equivalent.

anatomy of <<=

I'm trying to understand why hey needs the map call.

That's actually one of the difference macro now is able to handle automatically. To understand why map is needed in sbt 0.12 style, you need to understand the type of sbt DSL expression, which is Setting[_]. As Getting Started guide puts it:

Instead, the build definition creates a huge list of objects with type Setting[T] where T is the type of the value in the map. A Setting describes a transformation to the map, such as adding a new key-value pair or appending to an existing value.

For tasks, the type of DSL expression is Setting[Task[T]]. To turn a setting key into Setting[T], or to turn a task key into Setting[Task[T]], you use <<= method defined on respective keys. This is implemented in Structure.scala (sbt 0.12 code base has simpler implementation of <<= so I'll be using that as the reference.):

sealed trait SettingKey[T] extends ScopedTaskable[T] with KeyedInitialize[T] with Scoped.ScopingSetting[SettingKey[T]] with Scoped.DefinableSetting[T] with Scoped.ListSetting[T, Id] { ... }

sealed trait TaskKey[T] extends ScopedTaskable[T] with KeyedInitialize[Task[T]] with Scoped.ScopingSetting[TaskKey[T]] with Scoped.ListSetting[T, Task] with Scoped.DefinableTask[T] { ... }

object Scoped {
    sealed trait DefinableSetting[T] {
        final def <<= (app: Initialize[T]): Setting[T]  =  setting(scopedKey, app)
        ...
    }

    sealed trait DefinableTask[T] { self: TaskKey[T] =>
        def <<= (app: Initialize[Task[T]]): Setting[Task[T]]  =  Project.setting(scopedKey, app)
        ...
    }
}

Note the types of app parameters. Setting key's <<= takes Initialize[T] whereas the task key's <<= takes Initialize[Task[T]]. In other words, depending on the the type of lhs of an <<= expression the type of rhs changes. This requires sbt 0.12 users to be aware of the setting/task difference in the keys.

Suppose you have a setting key like description on the lhs, and suppose you want to depend on name setting and create a description. To create a setting dependency expression you use apply:

description <<= name { n => n + " is good." }

apply for a single key is implemented in Settings.scala:

sealed trait Keyed[S, T] extends Initialize[T]
{
    def transform: S => T
    final def apply[Z](g: T => Z): Initialize[Z] = new GetValue(scopedKey, g compose transform)
}
trait KeyedInitialize[T] extends Keyed[T, T] {
    final val transform = idFun[T]
}

Next, instead of description, suppose you want to create a setting for jarName in assembly. This is a task key, so rhs of <<= takes Initialize[Task[T]], so apply is not good. This is where map comes in:

jarName in assembly <<= name map { n => n + ".jar" }

This is implemented in Structure.scala as well:

final class RichInitialize[S](init: Initialize[S]) {
    def map[T](f: S => T): Initialize[Task[T]] = init(s => mktask(f(s)) )
}

Because a setting key extends KeyedInitialize[T], which is Initialize[T], and because there's an implicit conversion from Initialize[T] to RichInitialize[T] the above is available to name. This is an odd way of defining map since maps usually preserves the structure.

It might make more sense, if you see similar enrichment class for task keys:

final class RichInitializeTask[S](i: Initialize[Task[S]]) extends RichInitTaskBase[S, Task] {...}

sealed abstract class RichInitTaskBase[S, R[_]] {
    def map[T](f: S => T): Initialize[R[T]] = mapR(f compose successM)
}

So for tasks, map maps from a task of type S to T. For settings, we can think of it as: map is not defined on a setting, so it implicitly converts itself to a task and maps that. In any case, this let's sbt 0.12 users to think: Use apply for settings, map for tasks. Note that apply ever goes away for task keys as they extend Keyed[Task[T], Task[T]]. This should explain:

sbt-hello/build.sbt:21: error: type mismatch;
 found   : Unit
 required: sbt.Task[Unit]

Then there's the tuple issue. So far I've discussed dependencies to a single setting. If you want to depend on more, sbt implicitly adds apply and map to Tuple2..N to handle it. Now it's expanded to 15, but it used to be up till only Tuple9. Seeing from a new user's point of view, the idea of invoking map on a Tuple9 of settings so it generates a task-like Initialize[Task[T]] would appear alien. Without changing the underlying mechanism, sbt 0.13 provides much cleaner surface to get started.

like image 108
Eugene Yokota Avatar answered Oct 12 '22 03:10

Eugene Yokota