Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to "enrich" a Scala class without wrapping the code into another object?

In Scala 2.9 to add custom methods to a library class (enrich or "pimp" it) I had to write something like this:

object StringPimper {
  implicit def pimpString(s: String) = new {
    def greet: String = "Hello " + s
  }
}

As Scala 2.10 was released, I've read that it introduces implicit class definition which, theoretically, was meant to simplify the above task by removing the need in an implicit method returning an object of an anonymous class. I thought it was going to enable me to write just

implicit class PimpedString(s: String) {
  def greet: String = "Hello " + s
}

which looks much prettier for me. But, such a definition causes a compilation error:

`implicit' modifier cannot be used for top-level objects

which is solved by wrapping the code in an object again:

object StringPimper {
  implicit class PimpedString(s: String) {
    def greet: String = "Hello " + s
  }
}

needless to say, this almost neutralizes the sense of the improvement.

So, is there a way to write it shorter? To get rid of the wrapper object?

I actually have a MyApp.pimps package where all the pimps go (I don't have too many, I'd use some separate packages if I had) and I am sick of importing MyApp.pimps.StringPimper._ instead of MyApp.pimps.PimpedString or MyApp.pimps._. Of course I could put all the implicit classes in a single wrapper object, but this would mean to put them all in a single file, which would be pretty long - quite ugly solution.

like image 351
Ivan Avatar asked Feb 10 '13 20:02

Ivan


3 Answers

The standard term now is enrich. I'm not sure why it wasn't before, given that the library methods that used it were named things like RichString not PimpedString.

Anyway, you can't put implicit classes in nothing, but in order to use implicits you need methods and you can't put methods in nothing. But you can cheat to get everything in pimps.

// This can go in one file
trait ImplicitsStartingWithB {
  implicit class MyBoolean(val bool: Boolean) { def foo = !bool }
  implicit class MyByte(val byte: Byte) { def bar = byte*2 }
}

// This can go in another file
trait ImplicitsStartingWithS {
  implicit class MyShort(val short: Short) { def baz = -short }
  implicit class MyString(val st: String) { def bippy = st.reverse }
}

// This is a third file, if you want!
object AllImplicits extends ImplicitsStartingWithB with ImplicitsStartingWithS {}

scala> import AllImplicits._
import AllImplicits._

scala> true.foo
res0: Boolean = false

You can also do this with the pimps package object--typically you'll place a file called package.scala inside a directory called pimps, and then

package object pimps extends /* blah blah ... */ {
  /* more stuff here, if you need it */
}

The one caveat is that value classes, as a new feature, have a fair number of restrictions. Some are unnecessary, but if you want to avoid allocating a new MyBoolean class (which the JVM can often greatly optimize, but usually not to the degree of a bare method call) you'll have to work around the limitations. In that case,

// In some file, doesn't really matter where
class MyBoolean(val bool: Boolean) extends AnyVal { def foo = !bool }

package object pimps {
  def implicit EnrichWithMyBoolean(b: Boolean) = new MyBoolean(b)
}

which doesn't save you work (but does run faster).

like image 197
Rex Kerr Avatar answered Nov 20 '22 14:11

Rex Kerr


The work around for allowing the enrichment class to reside in a different file is to wrap it in a trait instead of an object:

// file: package.scala
package object mypackage extends StringImplicits {
  def test { println("Scala".greet) }
}

// file: StringImplicits.scala
package mypackage

trait StringImplicits {
  implicit class RichString(s: String) {
    def greet: String = "Hello " + s
  }
}

As Rex points out, you cannot use value classes inside other classes, though. So if you want to benefit from Scala 2.10's value classes you cannot use nested implicit classes. D'oh!

like image 36
0__ Avatar answered Nov 20 '22 14:11

0__


Well, the compiler error sais it all. You just can't define an implicit class at top-level. That means you either use a wrapper object (which can also be a package object), or define the class directly in the scope were it is meant to be used (in the rare case where it doesn't have to be reusable).

The reason for that is that you wouldn't be able to import the implicit conversion without the wrapper. You can't import the implicit class (precisely: the conversion) by its own name.

Besides that, I would suggest using a slightly different signature (see Value Classes):

implicit class PimpedString(val self: String) extends AnyVal

which has the effect that the calls to your extension methods can (and will) be inlined.

like image 3
Leo Avatar answered Nov 20 '22 13:11

Leo