aboutsummaryrefslogblamecommitdiff
path: root/doc/design.md
blob: 38e543499ce251795b34c241adf62ec1fe46be1e (plain) (tree)
1
2
3
4
5
6
        
 


                                                                     
 














































































                                                                                               

































                                                                          





















































                                                                                            
 
# Design

This chapter explains some of CBT's less obvious design decisions and
the reasons behind them. Hopefully this answers some of the questions
often unanswered when trying to understand a new tool.


## Why does CBT use inheritance?

First of all CBT uses classes, because needs some place to put tasks and
name them. Methods in classes are one way to do it. Methods calling each
other allows to effectively build a graph of dependent tasks.

Inheritance on top of that allows patching this graph to insert additional
steps, e.g. formatting the code before compiling it. Patching of the task
graph is what you do with sbt andso with CBT via inheritance/overrides.

This was also discussed in gitter here: https://gitter.im/cvogt/cbt?at=58a95663de50490822e869e5

Taking a graph an contrinuously patching it can be confusing, which is
why inheritance is confusing. You can even build non-terminating call
cycles. CBT's logic is not coupled to the inheritance layer. You can
write CBT builds without inheritance. They require a bit more code, but
may end up easier to maintain and understand.

## Task composition and aborting failed tasks and dependents

In CBT build tasks are methods. Task can depend on each
other by invoking each other. E.g. `package` is a method that invokes `compile`.
Build tasks can fail. By convention, if a task fails it is expected to throw
an Exception in order to also abort the execution of dependent tasks. When
`compile` finds compile errors it throws an exception and thereby also
drops out of the `package` execution pth. CBT catches and handles the
exception and returns control back to the user.

This design was chosen for simplicity and because build code lives in a
world with exceptions anyways as a lot of it is directly or indirectly
using java libraries. We might reconsider this design at some point. Or not.
The hope is that Exceptions are without major drawbacks and more
approachable to newcomers than monadic error handling, which would require
wrapper types and value composition via .map and for-expressions. Not using
a Monad however also means that CBT cannot reason about task composition
automatically, which means parallel task execution is not automatic, but
manual and opt-in where wanted.

## Why do CBT plugins use case classes where methods seem simpler?

In CBT plugins you may see

```
trait SomePlugin{
  def doSomething = SomePluginLib(...context).doSomething( a, b, ... )
}
case class SomePluginLib(...context...)(implicit ...more context...){
  case class doSomething(a: A, b: B, ...){
    def apply = {
      // logic
    }
  }
}
```

Why this? This seems much simpler:

```
trait SomePlugin{
  def doSomething = SomePluginLib.doSomething( a, b, ... )(...context)
}
object SomePluginLib
  def doSomething(a: A, b: B, ...)(...context...)(implicit ...more context...) = {
    // logic
  }
}
```

The reason is that the former allows easy patching while
the second does not. Let's pretend your build is this:
```
class Build(val context: Context) extends SomePlugin{
  override def doSomething = super.doSomething.copy( b = someOtherB )
}
```
Such a simple replacement of `b` while keeping all other arguments would
not be easily possible if doSomething was a def not a case class.

## Why do the libraries have ops packages and Module traits?

Java's and Scala's package system does allow importing things,
but not exporting things. Everything has to be imported
explicitly anywhere it is supposed to be used. It's just a
package system, not a module system. This leads to a lot of
import boiler plate. CBT tries to minimize the imports
necessary for it's use however. So how to we do this while
at the same time allowing modularization? In particular, how
do we do this with stand-alone methods and implicit classes
that have to be in an object, e.g. the package object?

Scala's traits can be used as a module system that supports exports.
This means we can take several modules (traits) and merge them into
something that exports everything defined in any of them. Basically
inheriting a trait means importing names into the scope of the
inheriting class and exporting those names to the class's users.

CBT's libraries define Module traits, which their package objects inherit.
This makes it easy to use the libraries by itself. CBT's core however
also inherits all of the library Module traits in it's package object,
meaning that by a simple `import cbt._` you get everything from all
libraries. This solves the import boiler plate.

For implicit classes it is a little bit trickier as those should
extend AnyVal for performance reasons, but that's only allowed in
an object, not a trait. So what we do instead is put Universal traits
(see http://docs.scala-lang.org/overviews/core/value-classes.html)
containing all the logic into a helper package `ops`. The package
objects that want to offer the implicit classes now define them
extending the Universal traits. This means a little boiler plate
where the package object is define, but also solves the import
boiler plate everywhere else.

## What is newBuild and why do we need it?

Methods in a class can call each other and thereby effectively form a graph.
Subclasses can patch this graph. Sometimes you want to create several
different variants of a graph. Let's say one building artifacts with a
-SNAPSHOT version and another one building a stable release. Or one with
scalaVersion 2.10 and one with scalaVersion 2.11. Or one optmizing your
code and one not optimizing it. You can subclass your build once for
each alternative. That's fine if you know which class to subclass, but
plugins for your build do not know what your concrete build class is
and cannot produce subclasses for it. Example

```
class Build(val context: Context){
  // this works fine
  def with210 = new Build(context){
    override def scalaVersion = "2.10"
  }
  def with211 = new Build(context){
    override def scalaVersion = "2.11"
  }
}
```
Imagine however
```
// defined by you:
class Build(val context: Context) extends CrossVersionPlugin
// defined by the plugin author:
trait CrossVersionPlugin{
  // this does not compile, Build does not exists when the plugin is compiled
  def with210 = new Build(context){
    override def scalaVersion = "2.10"
  }
  def with211 = new Build(context){
    override def scalaVersion = "2.11"
  }
}
```
Luckily CBT can cheat because it has a compiler available.
```
// defined by the plugin author:
trait CrossVersionPlugin{
  // method `newBuild` generates a new subclass of Build at runtime and creates an instance.
  def with210 = newBuild{"""
    override def scalaVersion = "2.10"
  """}
  def with211 = newBuild{"""
    override def scalaVersion = "2.11"
  """}
}
```

Problem solved. In fact this allows for a very, very flexible way of
creating differents variants of your build.