In my app, I am keeping track of the number of credits the user has. To add some type checking, I'm using a Credits
class similar to this one:
case class Credits(val numCredits: Int) extends Ordered[Credits] {
...
}
Suppose I have a function def accept(creds: Credits): Unit
that I want to call. Would there be a way for me to call it with
process(Credits(100))
process(0)
but not with this?
process(10)
I.e., I'd like to provide an implicit conversion only from the literal 0
and none other. Right now, I just have val Zero = Credits(0)
in the companion object and I think that's rather good practice, but I'd be interested in an answer anyway, including other comments, like:
Credits
rather extend AnyVal and not be a case class in 2.10?This kind of compile-time checking are the good terrain to use macros, which will be available in 2.10
A very smart guy named Jason Zaugg has already implemented something similar to what you need, but it applies to regex: Regex Compile Time checking.
You might want to look to its Macrocosm to see how it is done and how you could code your own macros with the same purpose.
https://github.com/retronym/macrocosm
If you really want to know more about Macros, firstly I would say that you need to be brave because the documentation is scarce for now and the API is likely to change. Jason Zaugg works compiles fine with 2.10-M3 but I am not sure it will works with the newer version.
If you want to start with some readings:
A good entry point is the scalamacros website http://scalamacros.org/ and the SIP document https://docs.google.com/document/d/1O879Iz-567FzVb8kw6N5OBpei9dnbW0ZaT7-XNSa6Cs/edit?pli=1
If you have time, you might also wants to read Eugene Burmako presentation: http://scalamacros.org/talks/2012-04-28-MetaprogrammingInScala210.pdf
Now, getting to the topic, Scala macros are CATs : "Compile-time AST Transformations". The abstract syntax tree is the way the compiler represents your source code. The compiler applies consequent transformations to the AST and at the last step it actual generates the java bytecode.
Let's now look to Jason Zaugg code:
def regex(s: String): scala.util.matching.Regex = macro regexImpl
def regexImpl(c: Context)(s: c.Expr[String]): c.Expr[scala.util.matching.Regex] = {
import c.universe._
s.tree match {
case Literal(Constant(string: String)) =>
string.r // just to check
c.reify(s.splice.r)
}
}
As you seen regex is a special function which takes a String and returns a Regex, by calling macro regexImpl
A macro function receives a context in the first parameter lists, and in second argument list the parameters of the macro under the form of c.Expr[A] and returns a c.Expr[B]. Please note that c.Expr is a path dependent type, i.e. it is a class defined inside the Context, so that if you have two context the following is illegal
val c1: context1.Expr[String] = ...
val c2: context2.Expr[String] = ...
val c3: context1.Expr[String] = context2.Expr[String] // illegal , compile error
Now if you look what happens in the code:
What's going on here is that there is an implicit conversion from string to StringOps defined in Predef.scala, which is automatically imported in the compilation every scala source
implicit def augmentString(x: String): StringOps = new StringOps(x)
StringOps extends scala.collection.immutable.StringLike, which contains:
def r: Regex = new Regex(toString)
Since macros are executed at compile time, this will be executed at compile time, and compilation will fail if an exception will be thrown (that is the behaviour of creating a regex from an invalid regex string)
Note: unluckily the API is very unstable, if you look at http://scalamacros.org/documentation/reference.html you will see a broken link towards the Context.scala. The right link is https://github.com/scala/scala/blob/2.10.x/src/reflect/scala/reflect/makro/Context.scala
Basically, you want dependent types. Why Scala supports a limited form of dependent types in path dependent types, it can't do what you ask.
Edmondo had a great idea in suggesting macros, but it has some limitations. Since it was pretty easy, I implemented it:
case class Credits(numCredits: Int)
object Credits {
implicit def toCredits(n: Int): Credits = macro toCreditsImpl
import scala.reflect.makro.Context
def toCreditsImpl(c: Context)(n: c.Expr[Int]): c.Expr[Credits] = {
import c.universe._
n.tree match {
case arg @ Literal(Constant(0)) =>
c.Expr(Apply(Select(Ident("Credits"), newTermName("apply")),
List(arg)))
case _ => c.abort(c.enclosingPosition, "Expected Credits or 0")
}
}
}
Then I started up REPL, defined accept
, and went through a basic demonstration:
scala> def accept(creds: Credits) { println(creds) }
accept: (creds: Credits)Unit
scala> accept(Credits(100))
Credits(100)
scala> accept(0)
Credits(0)
scala> accept(1)
<console>:9: error: Expected Credits or 0
accept(1)
^
Now, to the problem:
scala> val x = 0
x: Int = 0
scala> accept(x)
<console>:10: error: Expected Credits or 0
accept(x)
^
In other words, I can't track properties of the value assigned to identifiers, which is what dependent types would allow me to do.
But the whole strikes me as wasteful. Why do you want just 0 to be converted? It seems you want a default value, in which case the easiest solution is to use a default value:
scala> def accept(creds: Credits = Credits(0)) { println(creds) }
accept: (creds: Credits)Unit
scala> accept(Credits(100))
Credits(100)
scala> accept()
Credits(0)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With