Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Scala, can I implicitly convert only certain literals to my custom type?

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:

  • could this be done with macro implicit conversions in 2.10?
  • should Credits rather extend AnyVal and not be a case class in 2.10?
like image 940
Jean-Philippe Pellet Avatar asked Dec 07 '22 13:12

Jean-Philippe Pellet


2 Answers

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:

  • There is a match block which matches on s.tree
  • If s.tree is a Literal, containing a constant String , string.r is called

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

like image 78
Edmondo1984 Avatar answered Jan 31 '23 07:01

Edmondo1984


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)
like image 26
Daniel C. Sobral Avatar answered Jan 31 '23 07:01

Daniel C. Sobral