Scala 2.10.0 introduced support for macros to allow compile-time code execution of arbitrary Scala code. This lets us extend the behavior of the Scala compiler, and we can do a little "compile-time testing" of our code by letting the compiler prove the validity of our functions. When the proof fails to hold, the code does not compile.
Let's look at an overly simple constructive interpretation of an existential quantifier.
Our predicate is the squaring function, and we prove one of the possible outcomes, x x = 25, with x = 5*.
In Scala, this might look like a function which takes a unary function and an input/output pair.
val sq = constructive1({ x: Int => x * x }, (5,25))
// returns the function { x: Int => x * x }
Now we can use sq
to square integers:
val fiveTimesFive = sq(5)
The function constructive1
takes our squaring function
and an input/output pair, verifies that the input passed to the function
produces the output, and returns the verified function. We can then use
sq
, confident in the assumption that
sq(5) == 25
.
def constructive1[A, B](f: A => B, p: (A, B)): A => B =
[A, B]
macro constructive1_impl
def constructive1_impl[A, B](c: Context)( f: c.Expr[A => B]
, p: c.Expr[(A, B)]
): c.Expr[A => B] = {
val fe: c.Expr[A => B] = c.Expr[A => B](c.resetAllAttrs(f.tree))
val pe: c.Expr[(A, B)] = c.Expr[(A, B)](c.resetAllAttrs(p.tree))
val _f: A => B = c.eval(fe)
val _p: (A, B) = c.eval(pe)
val b = _f(_p._1)
if (b != _p._2)
.abort( c.enclosingPosition
c, "expected f(%s) = %s, but was %s".format(_p._1, _p._2, b)
)
.universe.reify(f.splice)
c}
Due to an implementation detail of the current Scala compiler, evaluation of arguments to a macro requires an untyped tree:
scala.tools.reflect.ToolBoxError: reflective toolbox has failed: cannot operate on trees that are already typed
To get around this, we wrap f.tree
in a call to
c.resetAllAttrs
. Unfortunately, this severely constrains
the arguments we can pass to our macro. For example, imagine we want to
define our squaring function outside of the macro call site:
def _sq(x: Int): Int = x * x
val sq = constructive1(_sq, (5,25))
This throws a
compiler error, due to the lack of type information resulting from
the c.resetAllAttrs
call above:
scala.tools.reflect.ToolBoxError: reflective compilation has failed: Example is not an enclosing class
For simple cases where the function under test can be defined entirely within the argument to the macro, this approach works well.
val sq = constructive1({ x: Int => x * x * x }, (5,25))
// fails to compile, because 5 * 5 * 5 != 25
val sq = constructive1({ x: Int => x * x }, (5,25))
// compiles, because 5 * 5 == 25
It would be neat to extend this approach by using ScalaCheck to throw lots of input/output pairs at the function under test.