0

I'm looking for the same behavior as the following OCaml code, where the compiler understands the match is exhaustive because we've expressed that the two scrutinees must have the same type:

type circle
type rectangle

type _ figure =
    | Circle : int -> circle figure
    | Rectangle : int * int -> rectangle figure

let equal_figure : type a. a figure -> a figure -> bool = fun f1 f2 -> match (f1, f2) with
| Circle r1, Circle r2 -> Int.(r1 = r2)
| Rectangle (x1, y1), Rectangle (x2, y2) -> Int.(x1 = x2 && y1 = y2)
(* the compiler knows this match is exhaustive *)

I can port the example directly to Scala and the exhaustiveness-checker does the right thing:

sealed trait CircleMarker
sealed trait RectangleMarker

enum Fig[T]:
  case Circle(r: Int) extends Fig[CircleMarker]
  case Rectangle(x: Int, y: Int) extends Fig[RectangleMarker]

def equalFig[T](f1: Fig[T], f2: Fig[T]): Boolean = (f1, f2) match
  case (Fig.Circle(r1), Fig.Circle(r2))               => r1 == r2
  case (Fig.Rectangle(x1, y1), Fig.Rectangle(x2, y2)) => x1 == x2 && y1 == y2
  (* the compiler knows this match is exhaustive *)

scastie

Is there a more succinct way to express this in Scala, without the phantom CircleMarker and RectangleMarker traits?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Max Heiber
  • 14,346
  • 12
  • 59
  • 97
  • 1
    If you're not instantiating/extensing `CircleMarker`, `RectangleMarker` they can be abstract types rather than traits https://scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA/4 Also you can try F-bounds https://scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA If you're not hiding implementation types (cases types) this can be just a sealed-trait hierarchy https://scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA/2 Actually, `equals` is already defined for case classes https://scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA/3 – Dmytro Mitin Mar 04 '23 at 10:50
  • The F-bounds example is perfect, if you submit it as an answer I can mark it accepted so it's discoverable. – Max Heiber Mar 04 '23 at 11:48
  • Re `equals`: it's a toy example, but there's an interesting difference between your last implementation and the original: in the original `equalFig(Fig.Circle(1), Fig.Rectangle(1, 2))` is ill-typed, but in your last example it's well-typed. – Max Heiber Mar 04 '23 at 11:53
  • 1
    *"but in your last example it's well-typed"* Sorry I don't understand. It seems to be still ill-typed https://scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA/10 – Dmytro Mitin Mar 04 '23 at 12:03
  • *"The F-bounds example is perfect, if you submit it as an answer I can mark it accepted"* done – Dmytro Mitin Mar 04 '23 at 12:08
  • I see, scastie.scala-lang.org/DmytroMitin/6oE7Jpe3Sxm5XFAgyT9ECA/10 is indeed ill-typed. I think scastie was funky on my phone – Max Heiber Mar 04 '23 at 18:30

1 Answers1

1

You can try F-bounds

enum Fig[T <: Fig[T]]:
  case Circle(r: Int) extends Fig[Circle]
  case Rectangle(x: Int, y: Int) extends Fig[Rectangle]

// sealed trait Fig[T <: Fig[T]]
// object Fig:
//   case class Circle(r: Int) extends Fig[Circle]
//   case class Rectangle(x: Int, y: Int) extends Fig[Rectangle]

def equalFig[T <: Fig[T]](f1: Fig[T], f2: Fig[T]): Boolean = (f1, f2) match
  case (Fig.Circle(r1), Fig.Circle(r2))               => r1 == r2
  case (Fig.Rectangle(x1, y1), Fig.Rectangle(x2, y2)) => x1 == x2 && y1 == y2

// def equalFig[T <: Fig[T]](f1: Fig[T], f2: Fig[T]): Boolean = f1 == f2

equalFig(Fig.Circle(1), Fig.Circle(1)) // true
equalFig(Fig.Circle(1), Fig.Circle(2)) // false
equalFig(Fig.Rectangle(1, 2), Fig.Rectangle(1, 2)) // true
equalFig(Fig.Rectangle(1, 2), Fig.Rectangle(1, 3)) // false
// equalFig(Fig.Circle(1), Fig.Rectangle(1, 2)) // doesn't compile
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66