Boilerless is a small utility that lets you write class hierarchies with a lightweight syntax closer to how you define data types in other functional languages. It has special support for Generalized Algebraic Data Types (GADT) and enums-like hierarchies.
Following is a short example showing how to write an EitherOrBoth
data type.
The precise rules used to expand it are explained further below.
@enum class EitherOrBoth[+A,+B] {
def fold[T](f: A => T, g: B => T)(m: (T,T) => T): T
// Cases:
class First [A](value: A) { fold(f,g)(m) = f(value) }
class Second[B](value: B) { fold(f,g)(m) = g(value) }
class Both[_](fst: A, snd: B) { fold(f,g)(m) = m(f(fst),g(snd)) }
}
Boilerless is based on macro annotations, which will expand at compile time into proper Scala code. The code above will generate the equivalent of:
sealed abstract class EitherOrBoth[+A, +B] {
def fold[T](f: A => T, g: B => T)(m: (T,T) => T): T
}
object EitherOrBoth {
// Cases:
case class First[+A](value: A) extends EitherOrBoth[A, Nothing] {
private[this] type B = Nothing
override def fold[T](f: A => T, g: B => T)(m: (T,T) => T): T = f(value)
}
case class Second[+B](value: B) extends EitherOrBoth[Nothing, B] {
private[this] type A = Nothing
override def fold[T](f: A => T, g: B => T)(m: (T,T) => T): T = g(value)
}
case class Both[+A, +B](fst: A, snd: B) extends EitherOrBoth[A, B] {
override def fold[T](f: A => T, g: B => T)(m: (T,T) => T): T = m(f(fst),g(snd))
}
}
Note: Macro annotations are not officially supported in Scala. Syntax highlighting may be broken in some IDE's. However, Boilerless offers alternatives to circumvent these problems.
Type and term parameters can be passed from case classes to the parent class implicitly
using the lightweight _[..types](...args)
syntax inside the body of the case class.
Moreover, if that is the very first expression in the body and there are no types to pass,
the _
can be ommitted. Therefore, one can write:
@enum class State(entryName: String) {
object Alabama {"AL"}
object Alaska {"AK"}
object California { _("CA") } // explicit initialization syntax
// and so on and so forth.
}
Boilerless has special support for enumeratum.
By only changing @enum
to @enumeratum
in the code above,
the parent class is made to extend enumeratum.EnumEntry
,
the case classes to extend enumeratum.Enum[State]
,
and a val values = findValues
field is added to the companion object:
@enumeratum class State(entryName: String) {
object Alabama {"AL"}
object Alaska {"AK"}
// and so on and so forth.
}
assert(State.withName("AL") == State.Alabama)
You can see the code generated by the definitions above here.
As shown in the EitherOrBoth
example above,
if there is no explicit extends Parent[..](...)
clause nor _[..](...)
initialization call,
type parameters named the same as type parameters of the parent class are forwarded automatically.
Bounds and variance annotations for these parameters do not need to be repeated,
as they are copied from the parent class.
Parent type parameters not mentioned in the case class are passed to the parent class
as the lower bound if the parameter is covariant, the upper bound if it is contravariant,
and an existential otherwise.
One can also import all parent parameters with syntax [_, ..]
,
i.e., first parameter named underscore _
, possibly followed by more parameters.
Additionally, a private type is created in each case class for all parent type parameters
it does not mention, so that it can refer to it nonetheless
(see EitherOrBoth
in cases First
and Second
).
Nested hierarchies are naturally supported, as macro annotations expand from the outermost to the innermost definition. The following:
@enum class Level0(x: Int) {
class Sub0(){0}
class Sub1(x: Int){x}
@enum class Level1(x: Int) { _(x)
class SubSub0{1}
class SubSub1(y: Int){y}
}
}
... generates:
sealed abstract class Level0(x: Int)
object Level0 {
case class Sub0() extends Level0(0)
case class Sub1(x: Int) extends Level0(x)
@enum case class Level1(x: Int) extends Level0(x) {
class SubSub0 {1}
class SubSub1(y: Int) {y}
}
}
... which in turn generates:
sealed abstract class Level0(x: Int)
object Level0 {
case class Sub0() extends Level0(0)
case class Sub1(x: Int) extends Level0(x)
sealed abstract class Level1(x: Int) extends Level0(x)
object Level1 {
case class SubSub0() extends Level1(1)
case class SubSub1(y: Int) extends Level1(y)
}
}
Boilerless has only been made to work on Scala 2.11 yet. More work is needed to port it to other versions.
To use Boilerless, enable the macro-paradise plugin and add the library dependency:
resolvers += Resolver.sonatypeRepo("snapshots")
addCompilerPlugin("org.scalamacros" % "paradise" % paradiseVersion cross CrossVersion.full)
libraryDependencies += "com.github.lptk" %% "boilerless" % boilerlessVersion
Where paradiseVersion
is the version of Macro Paradise (for example "2.1.0"
)
and boilerlessVersion
is the version of Boilerless (for example "0.1-SNAPSHOT"
).
See this project for an example.
Some IDE's like Eclipse seem to support Boilerless remarkably well – most type errors point to the right thing, and jump-to-definition is often approximately right.
Other IDE's like IntelliJ do not even try to understand macros.
To mitigate some of the IDE problems, you can make the companion object of the @enum
class extend the class,
so the IDE will at least see the case classes.
Boilerless also provides an @enumInFile(fileName, package)
macro that,
instead of expanding into the class trees, will write the result to a new Scala file [1].
The new file will be placed in $folderName/ClassName.scala
, its package will be $package
,
and imports found at macro call site will be placed at the top.
For example see this tests file, which contains:
@enumInFile("core/src/test/scala/boilerless/gen", "boilerless.gen")
class Opt[+T] { class Som[T](value: T); object Non }
The generated code can be found here.
Arguments folderName
and package
should be string literals.
It is advised to set folderName
to a folder belonging to a subproject
that depends on the project containing the @enum
class, and not the same project.
This way, whenever you change the @enum
class, it will re-expand first,
writing the result in the file located in the dependent project,
and that file will then be compiled as part of the dependent project.
Note: You may still have to compile twice,
unless you use a special configuration or command
to explicitly ask sbt
to compile the project containing the templates first,
like sbt templates-project/compile main-project/run
.
If you do not want to use macro annotations,
a def macro
version is also available as genEnum(folderName, package)(){""" code """}
.
This functionality has only been tested with sbt 0.13.8
and Scala 2.11.8
.
It is known not to work in IntelliJ
(but a mere warning will be raised and the macro failure will not stop compilation).
[1] Something macros are not supposed to do, but is very useful.
Here is a summary of Boilerless' functionalities:
-
Make outer class
sealed abstract
and remove potentialfinal
andcase
modifiers. -
Make inner classes and objects
final case
and move them to the companion object. -
Make inner classes extend outer class implicitly.
-
Forward type parameters with their bounds and variance if none are specified explicitly.
-
Pass type and term parameters to the parent class if specified with the
_[..](...)
syntax. -
Create private aliases to the arguments passed for the outer class' type parameters, if they are not also inner class parameters.
-
Convert expressions of the form
f(...args) = body
found in inner class bodies to definitions of the corresponding abstract methods or values found in the outer class.
Several options can be passed to @enum
in order to customize its behavior.
-
'Unseal
prevents making the@enum
class sealed. -
'NotInterested
removes warning like "this class could be an object!". -
'Debug
enables debugging output, and allows to see what is generated by the macro expansion.
For example: @enum('Unseal, 'Debug) class Enum { ... }
.
Annotate with @ignore
a member definition to leave it untouched by Boilerless.
In addition, arbitrary classes, methods and objects (even outside of @enum
hierarchies)
may be modified after the fact with:
-
@notCase
to cancel acase
modifier -
@open
to cancel afinal
orsealed
modifier -
@concrete
to cancel anabstract
modifier
Boilerless is completely syntax-driven,
as it operate before type-checking and name resolution.
As a consequence, if you extend the parent class explicitly with extends Base[..](...)
,
it is important to do so with the bare parent name (so Boilerless can detect it),
and not something like extends my.package.Base[..](...)
.
Some IDE's like IntelliJ will likely not understand Boilerless' syntax and semantics, so it may be good to turn inspections off for the specific definition files. See also this to circumvent the problem.