Pattern match exhaustivity warning about impossible case

Hi, I am getting a pattern match exhaustivity warning on a seemingly impossible case:

import scala.util.{Try,Success,Failure}

val Ok = Success(())

def f(x: Try[Unit]) = x match
  case Ok => 1
  case Failure(_) => 0

Warning:

match may not be exhaustive.

It would fail on pattern case: Success(_)

Playground: Scastie - An interactive playground for Scala.

However, since Ok: Success[Unit], it’s impossible for there to be any other case Success(_). Is this a known issue?

Not a solution for you, but it’s interesting that there’s no warning if you inline Success(())

import scala.util.{Try,Success,Failure}

def f(x: Try[Unit]) = x match
  case Success(()) => 1
  case Failure(_) => 0

Also not a solution, but I just encountered this when warning on calls to members of Unit.

You wouldn’t normally ().equals(x), but that is what a match case Success(()) entails.

In the example, case Ok always means Ok.equals, which for case class Success means isInstanceof[Success] and ().equals. Since the equality check is trivial, the preferred form is case _: Success[_].

Your reasoning isn’t wrong, but requires applying knowledge about final case classes to convert case Ok to case _: Success[_].

Not sure I completely understand your message. Are you saying that we should prefer to use case _: Success[_] instead of case Ok? Does Scala’s pattern matching exhaustivity check just not work for stable identifiers?

Could be wrong, but it looks like this is weirdness specific to Unit.

This also warns:

val Ok = ()

def f(x: Try[Unit]) = x match
  case Success(Ok) => 1
  case Failure(_) => 0

This works as expected:

sealed trait Ok
object Ok extends Ok

def f(x: Try[Ok]) = x match
  case Success(Ok) => 1
  case Failure(_) => 0

At least according to the Scaladoc, Unit isn’t really a value, so it kind of makes sense that pattern matching on it wouldn’t work. It’s an unpleasant surprise, to be sure, and maybe worth considering a special case to make it act more like a value in some of these cases.

There is only one value of type Unit, (), and it is not represented by any object in the underlying runtime system.


So I’m kind of wrong, because these variations also don’t work, and I’d expect the sealed trait exhaustiveness checking to have figured out that they are exhaustive:

sealed trait Done
case object Done extends Done

val Ok = Success(Done)

def f(x: Try[Done]) = x match
  case Ok => 1
  case Failure(_) => 0
sealed trait Done
object Done {
  case object Value extends Done
}

val Ok = Success(Done.Value)

def f(x: Try[Done]) = x match
  case Ok => 1
  case Failure(_) => 0
enum Done {
  case Value
}

val Ok = Success(Done.Value)

def f(x: Try[Done]) = x match
  case Ok => 1
  case Failure(_) => 0

You can kind of get something similar working by making an irrefutable extractor that covers all of Success[Unit]:

object Ok {
  def unapply(success: Success[Unit]): true = true
}

def f(x: Try[Unit]) = x match
  case Ok() => 1
  case Failure(_) => 0

Scastie

2 Likes

It would be nice if the compiler did not warn, or did not feel the need to warn.

The “spec” is at tuple patterns and not at literal patterns. It says it knows what () means.

case Success(_) means just test the type. case Success(()) means also ensure the value is the unit value, and if the value turns out not be what the (erased) type claims, then my pattern may not match. The purpose of the warning is to warn about that.

However, it would be nice if it took case Success(()) as case Success(_: ()) (which we aren’t allowed to write, for some reason), as the spec arguably implies. case Success(_: Unit) does not warn and also does not emit the trivial type test or equality.

1 Like