Jean-Philippe Pellet
Jean-Philippe Pellet

Reputation: 60006

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:

Upvotes: 4

Views: 829

Answers (3)

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297265

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)

Upvotes: 4

Edmondo
Edmondo

Reputation: 20090

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:

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

Upvotes: 10

Silas
Silas

Reputation: 1150

Use could use an implicit partial function:

scala> case class Credits(val numCredits: Int)
defined class Credits

scala> def process(c: Credits) = {}
process: (c: Credits)Unit

scala> implicit def i2c:PartialFunction[Int, Credits] = { case 0 => Credits(0) }

i2c: PartialFunction[Int,Credits]

Allows you

scala> process(Credits(12))

and

scala> process(0)

But:

scala> process(12)
scala.MatchError: 12 (of class java.lang.Integer)
        at $anonfun$i2c$1.apply(<console>:9)
        at $anonfun$i2c$1.apply(<console>:9)
        at .<init>(<console>:12)
        at .<clinit>(<console>)
        at .<init>(<console>:11)
        at .<clinit>(<console>)
        at $print(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Unknown Source)
        at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)

        at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.sca
la:920)
        at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:4
3)
        at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
        at java.lang.Thread.run(Unknown Source)

Edit: But yes, the compiler will still allow process(12) resulting in a match error at runtime.

Upvotes: 1

Related Questions