3

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?

Georg Heiler
  • 16,916
  • 36
  • 162
  • 292

1 Answers1

9

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.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • So I would need to create an implicit value reader? As currently, I get: could not find implicit value for evidence parameter of type net.ceedubs.ficus.readers.ValueReader - even adding `def loadConfiguration[T: ValueReader](implicit reader: ArbitraryTypeReader): T = {` does not fix it. Also importing `import net.ceedubs.ficus.readers.ArbitraryTypeReader._` for the caller will not yet resolve the problem. – Georg Heiler Dec 09 '17 at 20:05
  • No, you definitely don't need (or want) the implicit `ArbitraryTypeReader`, just the `ValueReader`. Given the definitions above, you can just import `ArbitraryTypeReader._` (at the use site, not where you're defining `loadConfiguration`) and call `loadConfiguration[EasyToDecode]`. – Travis Brown Dec 09 '17 at 20:09
  • I try to follow your instructions - but it does not seem to work yet. `ArbitraryTypeReader._` is imported on the caller sided i.e. `loadConfiguration[SomeCaseClass]` and the method looks just like you suggested, however, the error of: `Cannot generate a config ...` remains. Trying a `implicit val valueReader = ValueReader[HashingConfiguration]` fails. – Georg Heiler Dec 09 '17 at 20:14
  • @GeorgHeiler By "doesn't work" what do you mean? Does a simple case class (like the one in the answer) compile? – Travis Brown Dec 09 '17 at 20:15
  • Same error for `EasyToDecode` or my own case class. – Georg Heiler Dec 09 '17 at 20:16
  • 1
    That's odd then—`loadConfiguration[EasyToDecode]` compiles just fine for me (which is what I'd expect). You're importing both `Ficus._` and `ArbitraryTypeReader._`? – Travis Brown Dec 09 '17 at 20:22
  • Classes look like this: https://gist.github.com/geoHeil/a71c87bac767cba859faf817fca5315e if you do not spot the problem I can sin up a sample project on GitHub in a couple of minutes. – Georg Heiler Dec 09 '17 at 20:25
  • Actually, the error also specifies: `Cannot generate a config value reader for type String, because it has no apply method in a companion object that returns type String, and it doesn't have a primary constructor` - it feels a bit strange that ** String** would not be supported ... – Georg Heiler Dec 09 '17 at 20:43
  • `import net.ceedubs.ficus.Ficus._` - this also should be imported even if it marked as unsed by IDE – Igorock May 08 '20 at 16:33