How to implement fluent interface for scala subtypes?

Viewed 171

I can implement nested classes with fluent interfaces in the following fashion:

class Animal(name: String, props: Map[String, Any]) {
  def properties: Map[String, Any] = Map("name" -> name) ++ props
  def withAge(age: Int): Animal = new Animal(name, props.updated("age", age)) // can't use built-in copy
}

case class Cat(name: String, lives: Int, props: Map[String, Any]) extends Animal(name, props) {
  override def properties: Map[String, Any] = Map("name" -> name, "lives" -> lives) ++ props
  override def withAge(age: Int): Cat = Cat(name, lives, props.updated("age", age))
}

However I'm far from satisfied by this. There is a lot of repetition and, even if I'm using inheritance, I'm not reusing any code. I've tried using this.type as a return type and even using zio-prelude subtyping capability but the persistent problem is that, at some point, the subclass doesn't get recognized correctly.

Is there a better way to do this without repetition and leveraging scala features?

Ideally I would like something like this

case class Animal(name: String, props: Map[String, Any]) {
  def properties: Map[String, Any] = Map("name" -> name) ++ props
  def withAge(age: Int): this.type = copy(props = props.updated("age", age))
}

final case class Cat(name: String, lives: Int, props: Map[String, Any]) extends Animal(name, props + ("lives" -> lives))

so that no duplication is taking place. Of course the following is not compiling though.

val myCat: Cat = Cat("murzic", 9, Map()).withAge(4)
1 Answers
package animalworld

import scala.collection.mutable

class Builder(val properties: mutable.HashMap[String, Any]) {

  def this() = this(mutable.HashMap.empty)

  def withName(name: String): Builder = {
    properties.put("name", name)
    this
  }

  def withAge(age: Int): Builder = {
    properties.put("age", age)
    this
  }

  def setProperty(key: String, value: Any): Builder = {
    properties.put(key, value)
    this
  }

  def build[A <: Animal](implicit buildable: Buildable[A]): A =
    buildable.build(this)
}

package animalworld

trait Buildable[A <: Animal] {
  def build(builder: Builder): A
}
package animalworld

import scala.collection.mutable

class Animal protected (val name: String, val properties: Map[String, Any]) {
  def toBuilder: Builder = new Builder(mutable.HashMap.from(properties))
}

object Animal {
  implicit val animalBuildable: Buildable[Animal] = { builder =>
    new Animal(
      builder.properties("name").asInstanceOf[String],
      Map.from(builder.properties)
    )
  }
}
package animalworld

class Cat protected (
    override val name: String,
    val lives: Int,
    override val properties: Map[String, Any]
) extends Animal(name, properties)

object Cat {
  implicit val catBuildable: Buildable[Cat] = { builder =>
    new Cat(
      builder.properties("name").asInstanceOf[String],
      builder.properties("lives").asInstanceOf[Int],
      Map.from(builder.properties)
    )
  }
}
package animalworld

object Main extends App {
  val animal1 = new Builder().withName("tim-tim").build[Animal]

  val cat1 = animal1.toBuilder.withAge(10).setProperty("lives", 9).build[Cat]

  println(animal1.name)
  // tim-tim
  println(animal1.properties)
  // Map(name -> tim-tim)

  println(cat1.name)
  // tim-tim
  println(cat1.lives)
  // 9
  println(cat1.properties)
  // Map(name -> tim-tim, lives -> 9, age -> 10)
}

Related