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

4

16

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.

Lengthwise answered 10/2, 2013 at 20:58 Comment(0)
U
10

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).

Uel answered 10/2, 2013 at 21:27 Comment(4)
"given that the library methods that used it were named things like RichString not PimpedString" - that's exactly why I use "PimpedString" instead of "RichString" for my own String enrichment - to avoid name conflicts. I've used to use "Rich..." in the past but have given this practice up. But thank you for pointing to the standard term.Lengthwise
@Lengthwise - I use "Richer" for that reason. Point taken, though. (But there are a lot of synonyms. Enriched, Enhanced, Bling, Better, Awesome, etc..)Uel
@Mef - I think you forgot to use 2.10.Uel
The traits approach works well for me. It also points at where the implicits are coming from into scope.Demure
Z
5

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!

Zingaro answered 10/2, 2013 at 21:30 Comment(1)
Annoying, isn't it? I wonder if anyone's filed an enhancement request for that (hopefully for 2.11?). It's not like it would be a profoundly difficult change to relax the rules enough so that it would work without breaking value classes.Uel
S
3

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.

Sagerman answered 10/2, 2013 at 21:4 Comment(9)
I actually have a MyApp.pimps package where all the pimps go 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 a pretty long file - quite ugly solution.Lengthwise
@Lengthwise put them all into package object, yes, it will be long, but you don't have to write import MyApp.pimps.* then -- they will be available inside MyApp without every import statement.Foreshorten
@Foreshorten - the problem is, there is a mismatch currently in Scala. You can define the package object only once in one file, and then you must defined sealed traits altogether in one file. I have defended the restriction to non-toplevel scope before, but this has bitten me recently quite a lot. It's like: Do you want a sealed trait or do you want a package object, or do you want to put 10k loc in one file? I ended up using the old implicit conversion approach, this really undermines the idea of implicit classes IMO.Zingaro
I really-reall-really hate long code files, @om-nom-nom. Ideally a single code file should take less than a single screen page and every decently logically separate unit should be put in a separate file IMHO - this facilitates readability and version control. I also love C#'s "partial class" feature - it even lets to put different logically distinguished member groups of a single class in separate files.Lengthwise
@mef, why do you think I should consider using a value class here? I don't only pimp simple types like String or Int. I also pimp Joda-Time, JDBC, my own classes and case classes and even tuples.Lengthwise
there won't be an instantiation of your implicit class at runtime if you use value classesSagerman
But is it ok if the subject class is not a String but a usual complex class?Lengthwise
@Lengthwise 1) turns out value classes are not limited to primitive values only 2) String is not primitive anyways (it is complex in your terminology) ;-)Foreshorten
Oh, I see. I know String is a referential type but used to suspect it has a primitive counterpart in Java/JVM, @ForeshortenLengthwise
G
2

Another very good place to put implicit declaration is a companion object.

For an implicit conversion from Type1 to Type2 Scala looks in companion objects of all ancestors of both types. So it is often easy to place an implicit conversion somewhere that it won't need explicit import.

case class Curie(curie:String)

object Curie {
  implicit def toCurie(s:String) = Curie(curie)
}

Wherever you call a method that requires Curie with a String the conversion method is called without any additional import.

Gunboat answered 7/9, 2013 at 17:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.