Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Case class instance unexpectedly shared across map entries #1329

Open
arnegebert opened this issue Jun 13, 2022 · 3 comments
Open

Case class instance unexpectedly shared across map entries #1329

arnegebert opened this issue Jun 13, 2022 · 3 comments

Comments

@arnegebert
Copy link

  import pureconfig._
  import pureconfig.generic.auto._

  case class InnerConf() {
    lazy val value = {
      val value2 = scala.util.Random.nextInt(1000000)
      println(s"InnerConf has following value: $value2")
      value2
    }
  }
  case class OuterConf(middle: Map[String, MiddleConf] = Map.empty)

  case class MiddleConf(inner: InnerConf = InnerConf()) {
    println(s"inner-value: ${inner.value}, inner-hashcode ${inner.hashCode()}")
  }

  val res = ConfigSource.string("{middle.1 {}, middle.2 {}}").load[OuterConf]

Expectation: two separate instances of case class InnerConf are generated
Reality: the same instance of InnerConf is reused across the map entries "1" and "2" of middle

Example output:

InnerConf has following value: 51556
inner-value: 51556, inner-hashcode 2141524893
inner-value: 51556, inner-hashcode 2141524893

Is this intended behaviour? In my use case, I have a case class representing a random key-pair instead of InnerConf, and I would like to generate a random value when inner is not specified. I was surprised when the same object was being reused.

Apologies if there is a simpler way to reproduce the behaviour.

@leifwickland
Copy link
Collaborator

leifwickland commented Jun 13, 2022 via email

@jcazevedo
Copy link
Member

This is related to #1218. For default values, generic derivation using shapeless relies on a Default type class that exposes an HList of the default parameter values on the type constructor. This means that default values will be computed once we derive a Default instance. In your example, in order to derive a ConfigReader for MiddleConf we need a Default instance for MiddleConf. Once we have a Default instance for MiddleConf we've already eagerly evaluated the list of default values that will be used on all instances read from the derived ConfigReader.

If you're OK with not using generic derivation for MiddleConf you can define your own ConfigReader for it with the logic you pretend:

import pureconfig._
import pureconfig.generic.auto._

case class InnerConf() {
  lazy val value = {
    val value2 = scala.util.Random.nextInt(1000000)
    println(s"InnerConf has following value: $value2")
    value2
  }
}
case class OuterConf(middle: Map[String, MiddleConf] = Map.empty)

case class MiddleConf(inner: InnerConf = InnerConf()) {
  println(s"inner-value: ${inner.value}, inner-hashcode ${inner.hashCode()}")
}

object MiddleConf {
  implicit val middleConfReader: ConfigReader[MiddleConf] =
    ConfigReader.forProduct1[MiddleConf, Option[InnerConf]]("inner")(_.fold(MiddleConf())(MiddleConf(_)))
}

val res = ConfigSource.string("{middle.1 {}, middle.2 {}}").load[OuterConf]

However, my recommendation would be to avoid impure logic when loading the config. For your use case that means keeping your config domain models pure and handle the generation of random key-pairs separately.

@arnegebert
Copy link
Author

Thanks a lot for the clarifications! In my case, the easiest (backwards-compatible) solution I found is declaring inner as optional.
So something like the following:

  import pureconfig._
  import pureconfig.generic.auto._

  case class InnerConf() {
    lazy val value = {
      val value2 = scala.util.Random.nextInt(1000000)
      println(s"InnerConf has following value: $value2")
      value2
    }
  }
  case class OuterConf(middle: Map[String, MiddleConf] = Map.empty)

  case class MiddleConf(inner: Option[InnerConf] = None) {
    val innerValue = inner.getOrElse(InnerConf()).value
  }

  val res = ConfigSource.string("{middle.1 {}, middle.2 {}}").load[OuterConf]
  println(res.map(_.middle.values.map(_.innerValue)))

Example output:

InnerConf has following value: 18615
InnerConf has following value: 459547
Right(List(18615, 459547))

Apologies for the delayed response. From my side, this issue is resolved and it could be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants