Natural vampires
This Stack Overflow question is interesting—it asks whether we can use Scala macros to create a value class for positive integers where the positiveness is checked at compile-time, and where it's not possible to create an invalid instance.
I'm pretty sure it's not. My first thought was to turn PosInt
into a sealed universal trait
with a private value class implementation in the PosInt
companion object,
but inheriting from a universal trait forces us to give up most (all?) of the advantages of value classes in
this case, and of course it's not actually possible to make the value class private,
anyway.
So I don't have an answer, but I do have a pretty neat trick involving vampire methods that gives us some of the benefits of value classes.
If you've seen vampire methods before, the code will look pretty familiar:
import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.Context
class body(tree: Any) extends StaticAnnotation
object PosInt {
def apply(i: Int) = macro apply_impl
def apply_impl(c: Context)(i: c.Expr[Int]): c.Expr[Any] = {
import c.universe._
i.tree match {
case Literal(Constant(n: Int)) if n > 0 => c.Expr(
q"""
class Pos {
@body($n) def value: Int = macro PosInt.selectValue_impl
}
new Pos {}
"""
)
case Literal(Constant(n: Int)) => c.abort(
c.enclosingPosition,
"%d is not a positive integer!".format(n)
)
case _ => c.abort(c.enclosingPosition, "Not a literal!")
}
}
def selectValue_impl(c: Context) = c.Expr(
c.macroApplication.symbol.annotations.filter(
_.tpe <:< c.typeOf[body]
).head.scalaArgs.head
)
}
Now it's impossible to use the PosInt
constructor with a negative integer:
scala> val unthirteen = PosInt(-13)
<console>:28: error: -13 is not a positive integer!
val unthirteen = PosInt(-13)
^
When we give it a positive integer, we get a structurally-typed value:
scala> val thirteen = PosInt(13)
thirteen: AnyRef{def value: Int} = $anon$1@1814ac5c
This looks like bad news, but the neat part is that when we write something like this:
scala> thirteen.value.toString + "!!!"
res1: String = 13!!!
The expression is actually being rewritten at compile-time to 13.toString + "!!!"
.
No reflective access or even method calls of any kind—just a nice clean constant
in our code after macro expansion.
Again: it's definitely not an answer to the question, and it's nowhere near as nice as
a real value class would be—there's still an object allocation, for example—but it's an
example of how plain old (whitebox) def
macros
let us do something crazy without special language support.