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
useCompass
, so I don't have to duplicatetoList
andfromSeq
- achieve this with minimal changes in the codebase, i.e.
Landscape(sight1, sight2, sight3, sight4)
andLandscape.copy(west=someSight)
should still work, as well asLandscape.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 ofimport Landscape
- calls to landscape's custom factory methods other than
fromSeq
:LandscapeOps.pickByBeautifulSouth
instead ofLandscape.pickByBeautifulSouth