Scala: idiomatic way to chain multiple copy() updates to immutable case class

Viewed 45

I have an immutable configuration class, which can be only changed by constructing a copy of it using copy() call. The main for its construction is all good, but I also have an auxiliary flow, where configuration after construction can be modified based on a collection of modifiers.

This results in a following code:

  def updateConfigUsingMods(config: Config, mods: Modifiers): Config = {
    val config1 = if (mods.mod1) {
      config.copy(param1 = false, param2 = true)
    } else {
      config
    }

    val config2 = meta.mod2 match {
      case Some(value) => config1.copy(param3 = value)
      case None => config1
    }

    val config3 = meta.mod3 match {
      case Some(value) => config2.copy(param4 = value)
      case None => config2
    }

    // etc

    config3
  }

This chain of updates looks ugly and is incredibly error prone. The only other way I can think of is reserving responsibility and make Modifiers do the updates, but this looks ugly and overly verbose to my taste too:

case class Modifiers(mod1: Boolean, mod2: Option[Boolean], mod3: Option[String] {
  def updateConfigForMod1(config: Config): Config =
    if (mod1) {
      config.copy(param1 = false, param2 = true)
    } else {
      config
    }

  def updateConfigForMod2(config: Config): Config =
    mod2 match {
      case Some(value) => config.copy(param3 = value)
      case None => config
    }

  def updateConfigForMod3(config: Config): Config =
    mod3 match {
      case Some(value) => config.copy(param4 = value)
      case None => config
    }

  def updateConfig(config: Config): Config =
    updateConfigForMod1(config).updateConfigForMod2(config).updateConfigForMod3(config)
}

Is there a better way to achieve this?

1 Answers

The standard pattern I use is for the modifications to be methods on the Config class that return a modified Config. These can be chained by calling them in turn on the result of the previous modification.

The modification methods can have names that describe what the modification is doing. They are also are more tightly bound to the underlying config data, and they can be re-used.

[ This also uses fold to simplify the Option handling ]

case class Config(param1: Boolean, param2: Boolean, param3: Value, param4: Value)
{
  def withMod1(mod: Boolean) =
    if (mod) {
      this.copy(param1 = false, param2 = true)
    } else {
      this
    }

  def withMod2(mod: Option[Value]): Config =
    mod.fold(this)(value => this.copy(param3 = value))

  def withMod3(mod: Option[Value]): Config =
    mod.fold(this)(value => this.copy(param4 = value))
}

def updateConfigUsingMods(config: Config, mods: Modifiers): Config =
  config
    .withMod1(mods.mod1)
    .withMod2(mods.mod2)
    .withMod3(mods.mod3)

If the Config class cannot be changed these methods can be added via an implicit class.

Related