scalaenumspattern-matchinggadtnon-exhaustive-patterns

Succinct way to use GADTs for exhaustiveness-checking in Scala?


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?


Solution

  • 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