Loading a ficus configuration like
loadConfiguration[T <: Product](): T = {
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.ceedubs.ficus.Ficus._
val config: Config = ConfigFactory.load()
config.as[T]
fails with:
Cannot generate a config value reader for type T, because it has no apply method in a companion object that returns type T, and it doesn't have a primary constructor
when instead directly specifying a case class instead of T
i.e. SomeClass
it works just fine. What am I missing here?
Ficus uses the type class pattern, which allows you to constrain generic types by specifying operations that must be available for them. Ficus also provides type class instance "derivation", which in this case is powered by a macro that can inspect the structure of a specific case class-like type and automatically create a type class instance.
The problem in this case is that T
isn't a specific case class-like type—it's any old type that extends Product
, which could be something nice like this:
case class EasyToDecode(a: String, b: String, c: String)
But it could also be:
trait X extends Product {
val youWillNeverDecodeMe: String
}
The macro you've imported from ArbitraryTypeReader
has no idea at this point, since T
is generic here. So you'll need a different approach.
The relevant type class here is ValueReader
, and you could minimally change your code to something like the following to make sure T
has a ValueReader
instance (note that the T: ValueReader
syntax here is what's called a "context bound"):
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ValueReader
import com.typesafe.config.{ Config, ConfigFactory }
def loadConfiguration[T: ValueReader]: T = {
val config: Config = ConfigFactory.load()
config.as[T]
}
This specifies that T
must have a ValueReader
instance (which allows us to use .as[T]
) but says nothing else about T
, or about where its ValueReader
instance needs to come from.
The person calling this method with a concrete type MyType
then has several options. Ficus provides instances that are automatically available everywhere for many standard library types, so if MyType
is e.g. Int
, they're all set:
scala> ValueReader[Int]
res0: net.ceedubs.ficus.readers.ValueReader[Int] = net.ceedubs.ficus.readers.AnyValReaders$$anon$2@6fb00268
If MyType
is a custom type, then either they can manually define their own ValueReader[MyType]
instance, or they can import one that someone else has defined, or they can use generic derivation (which is what ArbitraryTypeReader
does).
The key point here is that the type class pattern allows you as the author of a generic method to specify the operations you need, without saying anything about how those operations will be defined for a concrete type. You just write T: ValueReader
, and your caller imports ArbitraryTypeReader
as needed.