Search code examples
scalagenericscase-classcompanion-objectenrich-my-library

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


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)

Solution

  • 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 =
        ???
    }