Home > OS >  How do I enrich a case class with minimal changes to the codebase?
How do I enrich a case class with minimal changes to the codebase?

Time:06-03

Using scala 2.11.12.

Scattered all over my code base I have a case class like this:

case class Landscape(
  north: Sight,
  east: Sight,
  south: Sight,
  west: Sight
) {
  def toList: List[Sight] = List(north, east, south, west)

  def isIdyllic: Boolean = north.isPastoral && east.isPastoral && south.isPastoral && west.isPastoral
}

(with a custom case class Sight) and a corresponding companion object:

object Landscape {
  def fromSeq(s: Seq[Sight]): Landscape = {
    require(s.length == 4)

    Landscape(
      north = s(0),
      east = s(1),
      south = s(2),
      west = s(3)
    )
  }

  def pickByBeautifulSouth(scape1: Landscape, scape2: Landscape): Landscape = {
    if (scape1.south.beauty > scape2.south.beauty) scape1 else scape2
  }
}

It turned out that it would be useful to have similar types, so I created a generic case class:

case class Compass[A](
  north: A,
  east: A,
  south: A,
  west: A
) {
  def toList: List[A] = List(north, east, south, west)
}

with a corresponding companion object:

object Compass {
  def fromSeq[A](s: Seq[A]): Compass[A] = {
    require(s.length == 4)

    Compass[A](
      north = s(0),
      east = s(1),
      south = s(2),
      west = s(3)
    )
  }
}

Obviously isIdyllic and pickByBeautifulSouth don't make sense for arbitrary types A. Now I'd like to make Landscape an enriched Compass, so I don't have to define toList and fromSeq in Landscape anymore.

I know I cannot do

case class Landscape(
  north: Sight,
  east: Sight,
  south: Sight,
  west: Sight
) extends Compass[Sight] {
  def isIdyllic: Boolean = north.isPastoral && east.isPastoral && south.isPastoral && west.isPastoral
}

since case-to-case inheritance is not possible. I also cannot make Compass[A] a trait like this:

trait Compass[A]{
  def north: A
  def east: A
  def south: A
  def west: A

  def toList: List[A] = List(north, east, south, west)
}

because that way I would break fromSeq which makes use of Compass's fields and its apply method.

I also thought of using an implicit class

implicit class LandscapeOps(ls: Compass[Sight]) {
  def isIdyllic: Boolean = ls.north.isPastoral && ls.east.isPastoral && ls.south.isPastoral && ls.west.isPastoral
}

and type-aliasing in my codebase

type Landscape = Compass[Sight]

however, this way I would again break my code by losing Landscape's apply method. And I also don't know how to add pickByBeautifulSouth.

Long story short: I'm looking for a way to

  • make Landscape use Compass, so I don't have to duplicate toList and fromSeq
  • achieve this with minimal changes in the codebase, i.e. Landscape(sight1, sight2, sight3, sight4) and Landscape.copy(west=someSight) should still work, as well as Landscape.pickByBeautifulSouth(scape1, scape2)

CodePudding user response:

Not sure if I understood all the limitations but .. Why not a trait Compass[T] and a subtype Landscape


trait Compass[A] {
  val north: A
  val east: A
  val south: A
  val west: A
  def toList: List[A] = List(north, east, south, west)
}

case class Landscape(
  north: Sight,
  east: Sight,
  south: Sight,
  west: Sight
) extends Compass[Sight] {
  def isIdyllic: Boolean = north.isPastoral && east.isPastoral && south.isPastoral && west.isPastoral
}

then the whole Landscape object still works

object Landscape {
  def fromSeq(s: Seq[Sight]): Landscape = {
    require(s.length == 4)

    Landscape(
      north = s(0),
      east = s(1),
      south = s(2),
      west = s(3)
    )
  }

  def pickByBeautifulSouth(scape1: Landscape, scape2: Landscape): Landscape =
    ???
}

CodePudding user response:

So here's a minimal working example of what I ended up with:

case class Sight() {
  def isPastoral: Boolean = true
  def beauty: Int = 5
}

case class Compass[A](
  north: A,
  east: A,
  south: A,
  west: A
) {
  def toList: List[A] = List(north, east, south, west)
}

object Compass {
  def fromSeq[A](s: Seq[A]): Compass[A] = {
    require(s.length == 4)

    Compass[A](
      north = s(0),
      east = s(1),
      south = s(2),
      west = s(3)
    )
  }
}

object LandscapeModule {
  type Landscape = Compass[Sight]
  val Landscape = Compass

  implicit class LandscapeOps(ls: Landscape) {
    import ls._

    def isIdyllic: Boolean = north.isPastoral && east.isPastoral && south.isPastoral && west.isPastoral
  }
  
  object LandscapeOps {
    def pickByBeautifulSouth(scape1: Landscape, scape2: Landscape): Landscape = {
      if (scape1.south.beauty > scape2.south.beauty) scape1 else scape2
    }
  }
}

object HowToUseIt {
  import LandscapeModule.{Landscape, LandscapeOps}

  val sight = Sight()
  val sights: Seq[Sight] = Seq.fill(4)(sight)

  val landscape: Landscape = Landscape(north = sight, east = sight, south = sight, west = sight)
  val landscapeFromSeq = Landscape.fromSeq(sights)

  LandscapeOps.pickByBeautifulSouth(landscape, landscapeFromSeq)
}

This way I only had to change two things in my codebase:

  • the imports: import LandscapeModule.{Landscape, LandscapeOps} instead of import Landscape
  • calls to landscape's custom factory methods other than fromSeq: LandscapeOps.pickByBeautifulSouth instead of Landscape.pickByBeautifulSouth
  • Related