In the example below, is it possible to write updateShape with the proper constraints so I don't need the three casts, the two in updateShape and the one at the call site?
trait ShapeModule:
module =>
type D
trait Shape:
def id: String
override def toString =
s"${module.getClass.getSimpleName()}>${getClass.getSimpleName}:$id"
class ShapeStart(val id: String, val dims: D) extends Shape:
def area: Double = module.area(dims)
def next: ShapeFinished = ShapeFinished(id, dims)
class ShapeFinished(id: String, dims: D) extends ShapeStart(id, dims):
override def area: Double = super.area + 10.0
def restart: ShapeStart = ShapeStart(id, dims)
def area(dims: D): Double
type ShapeBase = ShapeModule#Shape
case class Paper[S <: ShapeBase](shapes: Seq[S]):
def updateShape[SS <: S, ST <: ShapeBase, T <: ShapeBase](
id: String,
f: SS => ST
): Paper[T] =
val nextShapes =
shapes.map(s => if s.id == id then f(s.asInstanceOf[SS]) else s)
copy(shapes = nextShapes.asInstanceOf[Seq[T]])
object Circle extends ShapeModule:
type D = Double
def area(radius: Double) = Math.PI * radius * radius
object Rect extends ShapeModule:
type D = (Double, Double)
def area(dims: (Double, Double)) =
val (w, h) = dims
w * h
val circleStart = Circle.ShapeStart("C1", 2.0)
val rectStart = Rect.ShapeStart("R1", (2.0, 3.0))
val paperStart = Paper(Seq(circleStart, rectStart))
val paperFinish = paperStart.copy(
paperStart.shapes.map(_.next)
)
val paperStart2 = paperFinish.updateShape("C1", _.restart)
paperStart2.asInstanceOf[Paper[ShapeModule#ShapeStart]].shapes.map(_.area)
All three type parameters in updateShape would ideally be more specific. It seems there are two type hierarchies in this example, one in the inner Shape > ShapeStart > ShapeFinished hierarchy, and one described with type projections across all modules (e.g. ShapeModule#ShapeStart). Is there any relationship between these two that can be used to describe updateShape? Are there good blogs/papers on the subject? Thanks.
Since
ShapeStarthasdef areaandShapeFinishedhasdef area, it makes sense to adddef areatoShape, doesn't it?In
if s.id == id then f(s) else sthere's not much sense to considerf: S => S1because then-branch returnsS1while else-branch returnsS, so totally if-then-else returns the parent typeShapeBase(orS | S1). So it's enough to havef: S => ShapeBase, anyway.mapreturnsSeq[ShapeBase](orSeq[S | S1]).Otherwise if you want to return different subtypes from different branches then you need to know at compile time whether
s.id == idi.e. to moveidto type level. If you want to return different subtypes for different elements of the collection thenSeqis not a proper data type, you need heterogeneous list (Tuple) and polymorphic function.I tried to minimize the number of generics:
The reason to have more generics is additional type safety, but additional type safety is an illusion if you have to cast.
Or we can move the default implementation of
areatoShapeif we adddef dimstoShapesince bothShapeStartandShapeFinishedhavedef dimsIf you can't add
areatoShapebecause there can be other inheritors ofShapewithoutareathen here is an implementation with union typesSince
ShapeFinishedextendsShapeStart,ShapeModule#ShapeStart | ShapeModule#ShapeFinishedis justShapeModule#ShapeStart.Since you apply
updateShapeto_.restart : ShapeFinished => ShapeStarti.e.S=ShapeFinished,S1=ShapeStart,ShapeFinished <: ShapeStart, we can add boundS1 >: S, thenS | S1 = S1Maybe that's what you were looking for.
Most general signature is
But then you'll have to specify the type of lambda:
(_: ShapeModule#ShapeFinished).restartinstead of just_.restart.