Skip to content

Instantly share code, notes, and snippets.

@eneveu
Last active January 7, 2019 14:32
Show Gist options
  • Save eneveu/32c410382addc8c22331c3064cb06d71 to your computer and use it in GitHub Desktop.
Save eneveu/32c410382addc8c22331c3064cb06d71 to your computer and use it in GitHub Desktop.
Should I use "implicit def" or "implicit class" for an API ?
package implicitclass
import scala.collection.JavaConverters._
import scala.collection.immutable._
import com.typesafe.config.{ Config, ConfigFactory }
trait ConfigSupport {
def customLoadConfig(): Config = ConfigFactory.load()
}
object ConfigSupport extends ConfigSupport
object ConfigImplicits {
implicit class RichConfig(val config: Config) extends AnyVal {
def toMap: Map[String, String] = {
config.entrySet().asScala.map(entry ⇒ (entry.getKey, entry.getValue.unwrapped().toString)).toMap
}
}
}
object UsageWithTrait extends ConfigSupport {
def test(): Unit = {
import ConfigImplicits._
val config = customLoadConfig()
val map = config.toMap
}
}
object UsageWithObject {
def test(): Unit = {
import ConfigImplicits._
import ConfigSupport._
val config = customLoadConfig()
val map = config.toMap
}
}
package implicitdef
import scala.collection.JavaConverters._
import scala.collection.immutable._
import com.typesafe.config.{ Config, ConfigFactory }
trait ConfigSupport {
def customLoadConfig(): Config = ConfigFactory.load()
implicit def enrichConfig(config: Config): RichConfig = new RichConfig(config)
}
object ConfigSupport extends ConfigSupport
class RichConfig(val config: Config) extends AnyVal {
def toMap: Map[String, String] = {
config.entrySet().asScala.map(entry ⇒ (entry.getKey, entry.getValue.unwrapped().toString)).toMap
}
}
object UsageWithTrait extends ConfigSupport {
def test(): Unit = {
val config = customLoadConfig()
val map = config.toMap
}
}
object UsageWithObject {
def test(): Unit = {
import ConfigSupport._
val config = customLoadConfig()
val map = config.toMap
}
}
package notraitimplicitclass
import scala.collection.JavaConverters._
import scala.collection.immutable._
import com.typesafe.config.{ Config, ConfigFactory }
object ConfigSupport {
def customLoadConfig(): Config = ConfigFactory.load()
implicit class RichConfig(val config: Config) extends AnyVal {
def toMap: Map[String, String] = {
config.entrySet().asScala.map(entry ⇒ (entry.getKey, entry.getValue.unwrapped().toString)).toMap
}
}
}
object Usage {
def test(): Unit = {
import ConfigSupport._
val config = customLoadConfig()
val map = config.toMap
}
}
@eneveu
Copy link
Author

eneveu commented Jan 7, 2019

I'm designing an internal library to simplify config code in our projects. This library will have utility methods (e.g. def customLoadConfig()) and implicit extension methods (e.g. RichConfig#toMap()).

I often see a pattern of providing a trait Foo and a companion object extending that trait, to let users choose between extends Foo or import Foo._ (useful in the Scala REPL). There is an example of this in Scalatra. But I can't put an implicit class Xxx extends AnyVal inside a trait, because value classes "must be a top-level class or a member of a statically accessible object".

I have multiple choices :

  • move the implicit class to a separate object, and force users to specifically import that object (see ImplicitClass.scala)
  • use an implicit def instead, and move the RichConfig outside the trait (see ImplicitDef.scala)
  • stop using a trait and just provide a ConfigSupport object that users will import (see NoTraitImplicitClass.scala)
  • stop extending AnyVal and take the negligibel performance hit (in this case, it wouldn't be a problem, since configuration code is usually called only once at startup, but it could be more problematic in performance-critical code that is called in a loop)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment