scalagenericsconfigurationpureconfig

Using generic type with PureConfig in Scala


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.


Solution

  • 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:

    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.