Elegant grouping of implicit value classes
Asked Answered
Z

1

10

I'm writing a set of implicit Scala wrapper classes for an existing Java library (so that I can decorate that library to make it more convenient for Scala developers).

As a trivial example, let's say that the Java library (which I can't modify) has a class such as the following:

public class Value<T> {
    // Etc.
    public void setValue(T newValue) {...}
    public T getValue() {...}
}

Now let's say I want to decorate this class with Scala-style getters and setters. I can do this with the following implicit class:

final implicit class RichValue[T](private val v: Value[T])
extends AnyVal {
  // Etc.
  def value: T = v.getValue
  def value_=(newValue: T): Unit = v.setValue(newValue)
}

The implicit keyword tells the Scala compiler that it can convert instances of Value to be instances of RichValue implicitly (provided that the latter is in scope). So now I can apply methods defined within RichValue to instances of Value. For example:

def increment(v: Value[Int]): Unit = {
  v.value = v.value + 1
}

(Agreed, this isn't very nice code, and is not exactly functional. I'm just trying to demonstrate a simple use case.)

Unfortunately, Scala does not allow implicit classes to be top-level, so they must be defined within a package object, object, class or trait and not just in a package. (I have no idea why this restriction is necessary, but I assume it's for compatibility with implicit conversion functions.)

However, I'm also extending RichValue from AnyVal to make this a value class. If you're not familiar with them, they allow the Scala compiler to make allocation optimizations. Specifically, the compiler does not always need to create instances of RichValue, and can operate directly on the value class's constructor argument.

In other words, there's very little performance overhead from using a Scala implicit value class as a wrapper, which is nice. :-)

However, a major restriction of value classes is that they cannot be defined within a class or a trait; they can only be members of packages, package objects or objects. (This is so that they do not need to maintain a pointer to the outer class instance.)

An implicit value class must honor both sets of constraints, so it can only be defined within a package object or an object.

And therein lies the problem. The library I'm wrapping contains a deep hierarchy of packages with a huge number of classes and interfaces. Ideally, I want to be able to import my wrapper classes with a single import statement, such as:

import mylib.implicits._

to make using them as simple as possible.

The only way I can currently see of achieving this is to put all of my implicit value class definitions inside a single package object (or object) within a single source file:

package mylib
package object implicits {

  implicit final class RichValue[T](private val v: Value[T])
  extends AnyVal {
    // ...
  }

  // Etc. with hundreds of other such classes.
}

However, that's far from ideal, and I would prefer to mirror the package structure of the target library, yet still bring everything into scope via a single import statement.

Is there a straightforward way of achieving this that doesn't sacrifice any of the benefits of this approach?

(For example, I know that if I forego making these wrappers value classes, then I can define them within a number of different traits - one for each component package - and have my root package object extend all of them, bringing everything into scope through a single import, but I don't want to sacrifice performance for convenience.)

Zither answered 12/2, 2018 at 23:33 Comment(0)
P
5
implicit final class RichValue[T](private val v: Value[T]) extends AnyVal

Is essentially a syntax sugar for the following two definitions

import scala.language.implicitConversions // or use a compiler flag

final class RichValue[T](private val v: Value[T]) extends AnyVal
@inline implicit def RichValue[T](v: Value[T]): RichValue[T] = new RichValue(v)

(which, you might see, is why implicit classes have to be inside traits, objects or classes: they also have matching def)

There is nothing that requires those two definitions to live together. You can put them into separate objects:

object wrappedLibValues {
  final class RichValue[T](private val v: Value[T]) extends AnyVal {
    // lots of implementation code here
  }
}

object implicits {
  @inline implicit def RichValue[T](v: Value[T]): wrappedLibValues.RichValue[T] = new wrappedLibValues.RichValue(v)
}

Or into traits:

object wrappedLibValues {
  final class RichValue[T](private val v: Value[T]) extends AnyVal {
    // implementation here
  }

  trait Conversions {
    @inline implicit def RichValue[T](v: Value[T]): RichValue[T] = new RichValue(v)
  }
}

object implicits extends wrappedLibValues.Conversions
Pernell answered 13/2, 2018 at 6:31 Comment(3)
I'm not sure that this is as convenient as I was hoping for. :-) According to SIP-13 (implicit classes), the implicit conversion function isn't @inlined automatically - where do you get that from? Also, by explicitly creating a instance of the wrapper class - even if @inlined - do you still get the performance benefit? SIP-15 (value classes) suggests not. I guess some benchmarking might be in order...Zither
@MikeAllen yeah, I was wrong on inlining (tho it does not hurt). And yes, by explicitly creating an instance you still get the benefit. E.g. here with -Xprint:jvm compiler option you can see the call transformed to Foo.printInt$extensionPernell
OK, I guess that makes sense. I'm accepting your answer, but - in truth - I no longer technically have implicit value classes, but implicit conversion functions and value classes, so the solution for each wrapper is far more verbose and, potentially, error prone. :-( But, at least I can organize my code better. :-) I guess a better solution, building on this, would be to use a macro to expand class declarations...Zither

© 2022 - 2024 — McMap. All rights reserved.