This page discusses the coding style and other guidelines for sbt 1.0.
sbt 1.0 will primarily target Scala 2.11. We will cross build against Scala 2.10.
Before 1.0 is release, we should clean up deprecations.
On Scala 2.11 we should aim for zero warnings. One exception may be deprecation if it’s required for 2.10 cross building.
The fewer methods we can expose to the build user, the easier it becomes to maintain it.
Code against interface.
The implementation details should be hidden behind sbt.internal.x
package,
where x
could be the name of the main package (like io
).
Making independent modules with fewer dependent libraries make it easier to reuse them.
Avoid exposing external classes out to API, except for standard Scala and Java classes.
A module may be declared internal if there’s no use for public.
-encoding utf8
-deprecation
-feature
-unchecked
-Xlint
-language:higherKinds
-language:implicitConversions
-Xfuture
-Yinline-warnings
-Yno-adapted-args
-Ywarn-dead-code
-Ywarn-numeric-widen
-Ywarn-value-discard
-Xfatal-warnings
The -Xfatal-warnings
may be removed if there are unavoidable warnings.
Use the package name appended with the layer name, such as sbt.io
for IO layer.
The organization name for the publish artifacts should remain org.scala-sbt
.
A good overview on the topic of binary resiliency is Josh's 2012 talk Binary resiliency. The guideline here applies mostly to publicly exposed APIs.
Use MiMa.
def
declarations only val
or var
in trait
results to codegen at subclass and at the artificial Foo$class.$init$
.
lazy val
results to codegen at subclass
To trait, or not to trait?. It is less flexible compared to trait, but easier to maintain binary compatibility. It also has better Java interop.
If there’s no need to keep the classes open, seal it.
If there’s no need to keep the classes open, finalize it.
Typeclass pattern with pure trait might be easier to maintain binary compatibility than subclassing.
case class involves various codegen, which makes it harder to maintain binary compatibility over time.
The default parameter values are effectively codegen, which makes it difficult to maintain.
Here are other guidelines about public API.
Define datatypes.
def apply
def apply
should be reserved for factory methods
in a companion object that returns type T
.
Vector
, List
, or Array
) rather than Seq
. scala.Seq
is scala.collection.Seq
, which is not immutable.
Default to Vector
. Use List
if constant prepending is needed.
Use Array
if Java interop is needed.
Note using mutable collection is perfectly fine within the implementation.
toSeq
or anything side-effecty on Set
Set
is fine if you stick with set operations, like contains
and subsetOf
.
More often than not, toSeq
is called explicitly or implicitly,
or some side effecting method is called from map
.
This introduces non-determinism to the code.
toSeq
on Map
Same as above. That will introduce non-determinism.
Instead of functions and tuples, turn them into a trait. This is if Java interop is a concern, like implementing incremental compiler.
sbt-houserules comes with scalariform.
Return Unit
.
final class FooID {}
object FooID {
implicit val fooIdPicklerUnpicker: PicklerUnpickler[FooID] = ???
}
Avoid defining implicit converters in companion objects and package objects.
Suppose IO module introduces URL
enrichment called RichURI
;
and LibraryManagement introduces String
enrichment called GroupID
(for ModuleID
syntax).
These implicit converters should be defined in an object named syntax
in respective packge:
package sbt.io
object syntax {
implicit def uriToRichURI(uri: URI): RichURI = new RichURI(uri)
}
When all the layers are available, sbt
package should also define an object called syntax
,
which forwards all the implicit converters from all the layers:
package sbt
object syntax {
implicit def uriToRichURI(uri: URI): io.RichURI = io.syntax.uriToRichURI(uri)
....
}