Home > other >  Using generic type with PureConfig in Scala
Using generic type with PureConfig in Scala

Time:12-21

I'm trying to call PureConfig's loadOrThrow from method with generic type:

def load[T: ClassTag](path: String): T = {
    import pureconfig.generic.auto._
    ConfigSource.file(path).loadOrThrow[T]
}

When I try to call it from main class, i got following error:

could not find implicit value for parameter reader: pureconfig.ConfigReader[T]
    ConfigSource.file(path).loadOrThrow[T]

Can I fix this without import pureconfig.generic.auto._ in main class.

CodePudding user response:

To summarize comments and explain how this codec thing works.

When you do:

def something[T: ConfigReader] = ...

you are using syntax sugar for

// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...

// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...

On the call site when you write:

something[T]

compiler actually does

something(configReaderForT /* : ConfigReader[T] */)

So basically it is type-based dependency injection supported by compiler. And dependency injection has to get the value to pass from somewhere.

How can compiler obtain that value to pass it over? It has to find it by its type in the scope. There should be one, unambiguously nearest value (or def returning this value) of this type marked as implicit (Scala 2) or given (Scala 3).

// Scala 2
implicit val fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader

// Scala 3
given fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader

Scala 3 basically made it easier to distinguish which is the definition of value - given - and which is the place that relies on providing value from somewhere external - using. Scala 2 has one word for it - implicit - which was a source of a lot of confusion.

You have to define this value/method yourself or import it - in the scope that requires it - otherwise compiler will only try to look into companion objects of all types that contribute to your type T - if T is specific. (Or fail if it cannot find it anywhere like in your compiler error message).

// Example of companion object approach
// This is type Foo
case class Foo()
// This is Foo's companion object
object Foo {
  // This approach (calling derivation manually) is called semiauto
  // and it usually needs a separate import
  import pureconfig.generic.semiauto._

  implicit val configReader: ConfigReader[Foo] = deriveReader[Foo]
}

// By requiring ConfigReader[Foo] (if it wasn't defined/imported
// into the scope that needs it) compiler would look into:
// * ConfigReader companion object
// * Foo companion object
// ConfigReader doesn't have such instance but Foo does.

If T is generic, then you have to pass that implicit/given as a parameter - but then you are only deferring the moment where you have to specify it and let the compiler find/generate it.

// Tells compiler to use value passed as parameter
// as it wouldn't be able to generate it based on generic information

// implicit/using expressed as "type bounds" (valid in Scala 2 and 3)
def something[T: ConfigReader] = ...
// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...
// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...

// It works the same for class constructors.

In PureConfig's case, pureconfig.generic.auto contains implicit defs which generate the value for a specified T. If you want to have it generated, you have to import it in the place which will turn require that specific instance. You might do it in a companion object, to make it auto-importable wherever this ConfigReader would be needed for this particular type or import it in main (or any other place which specifies the T to something). One way or the other, you will have to derive it somewhere and then add this [T: ConfigReader] or (implicit configReader: ConfigReader[T]) in signatures of all the methods which shouldn't hardcode T to anything.

Summarizing your options are:

  • leave the import in the main (if you are hardcoding T to a specific type in the main)
  • derive it and define as implicit somewhere else and then import it from there (some people do it in traits and then mix-in them, but I am not a fan of this approach)
  • derive it and define as implicit in companion object

As long as you want your configs to be parsed values rather than untyped JSON (HOCON) without writing these codecs yourself, you have to perform that automatic (or semiautomatic) derivation somewhere.

  • Related