Correct way to intentionally error in pattern matching

At least while I’m writing the program, I’m not yet 100% convinced that my pattern matching is exhausting. In this case what is the correct way to intentionally trigger an error at runtime if the unexpected case arrives? I’d like to do something like the following call to error. Is there a good idiom to use?

    val r:(Int,A) = stack match {
      case Nil => error(s"I don't think this will ever happen: objs=$objs, init=$init")
      case head::tail => tail.foldLeft(head){case (a1,(_,a2)) => (0,f(a1,a2))}
    }

I don’t know if this is the correct way, but what I do is throw an exception, e.g.

val r:(Int,A) = stack match {
      case Nil => throw new Exception(s"I don't think this will ever happen: objs=$objs, init=$init")
      case head::tail => tail.foldLeft(head){case (a1,(_,a2)) => (0,f(a1,a2))}
    }

To make debugging easier, I usually have a custom class extending Exception which has as arguments relevant stuff in the environment (an example copied from my code is below.

class ApplnFailException(val func: Term, val arg: Term)
      extends IllegalArgumentException(
        s"function $func with domain(optional) ${ApplnFailException
          .domOpt(func)} cannot act on given term $arg with type ${arg.typ}"
      ) {
    def domOpt: Option[Typ[Term]] = func match {
      case fn: FuncLike[u, v] => Some(fn.dom)
      case _                  => None
    }

    def argType: Typ[Term] = arg.typ
  }

regards,
Siddhartha

As a rule of thumb, I recommend putting the expected cases at the top where possible, and a catch-all case _ => at the bottom for unexpected cases, if you’re not sure about whether exhaustivity checking will work in your situation.

That said, if stack is a List here, then what you have looks fine – it’s either a cons cell or Nil, so you’ve covered your bases…

@jducoeur, in this particular case, I think my program logic prevents Nil from ever occurring. But, in this situation, I wanted to write some tests to verify my reasoning. BTW, the function error is not found. Perhaps I need to import something?

Hmmm, that it interesting, because it is the opposite of my rule of thumb. My rule of thumb is DON’T put in an _ case for unexpected cases, because that will circumvent the compiler’s ability to warn me about missing cases. I generally try to make the pattern matching as specific as possible, to get the maximum benefit from the compiler diagnostics.

You don’t need to import anything, and can just prefix it with the required package, i.e.

sys.error("My message")

Regarding the initial question on the proper way of intentionally fail in a pattern-matching, I think using sys.error is a good trade-off: it has a pretty light-weight syntax, and it doesn’t pollute your code with lots of checked exceptions. And its return type Nothing makes it very easy to use.

I have been looking for alternatives, either built-in or available in the standard library, but did not find anything that was as concise as sys.error. More experienced users may have further advice.

You could also consider using requires/ensures clauses to better assess your program logic, but that wouldn’t address the problem of making your pattern-matchings exhaustive.

1 Like

Agreed.

An unqualified wildcard match breaks exhaustivity checking and I’ve seen it lead to production issues more than once when accidentally (or intentionally!) left in place, so i tend to consider it an anti-pattern. Especially with ADTs, when the base type is modified, you WANT the use sites to be updated (or at least looked at).

I’m confused what is the goal here. A failing match already throws a MatchError.

I will propose an exception to this rule. In untyped Akka code, the messages are of type Any and messages that aren’t handled are just silently ignored. Having a last case that accepts anything any prints out a message saying that you got a message you didn’t handle can save tons of time for finding and tracking down bugs. Of course, this exception to the rule disappears if you being using Akka Typed.

Hence the “if you’re not sure about whether exhausivity checking will work”. When you have a pattern that should be exhaustive (typically a sealed trait), then I agree with you. But you often aren’t in a situation where that is possible, so I generally prefer to put in the case _, and handle the error in a custom way, instead of risking a MatchError.

(I may be more sensitive to this than average – my code is extremely Akka-heavy, and as @MarkCLewis points out, that’s the absolute worst-case situation when it comes to exhaustivity checking.)

@jducoeur, perhaps another common exception is tuple types.
Here is a case where I WANT a match error in case I’ve forgotten a case.

object And extends BinaryOperation {
  def apply():Bdd = BddTrue

  def apply(bdd:Bdd):Bdd = bdd

  def apply(b1: Bdd, b2: Bdd): Bdd = {
    (b1, b2) match {
      case (b1, b2) if b1 eq b2 => b1
      case (BddTrue, _) => b2
      case (BddFalse, _) => BddFalse
      case (_, BddTrue) => b1
      case (_, BddFalse) => BddFalse
      case (b1: BddNode, b2: BddNode) => Bdd.bddOp(this, b1, b2)
    }
  }
}

Here is another case. By the way, in both of these cases it would be even better if the compiler would warn me about unreachable code, in case I’ve messed up in the order of the clauses.

  def treeReduce[A](objs:List[A],init:A,f:(A,A)=>A):A = {
    def recur(stack:List[(Int,A)],remaining:List[A]):A = {
      (stack,remaining) match {
        case ((a,x)::(b,y)::tail,_) if a == b => recur((a+1,f(x,y))::tail,remaining)
        case (_,o::os) => recur((1,o)::stack,os)
        case ((_,o)::Nil,Nil) => o
        case ((_,x)::(_,y)::tail,Nil) => recur((0,f(x,y))::tail, Nil)
      }
    }
    recur((1,init)::Nil,objs)
  }

Sure. But the absence of an unreachable pattern in the code can have so many meanings.

By using sys.error, the failing case is documented in the code. And you can add some information to debug if the situation occurs when it actually shouldn’t.

That’s fine. Don’t get me wrong – I’m not saying “You must always have a catch-all case clause”. I’m just saying that I prefer custom error handlers to the catch-all MatchError (so that production failures get handled appropriately), if I think that a missing case is even possible. (Which it shouldn’t be for well-behaved ADTs.)

Well, there is one reason to add such clauses: stop the compiler from warning you of inexhaustive matching. So I do sometimes have clauses like:

case _ => throw new RuntimeException(“This should ever happen. Must be a bug in the code.”)

Ooh. I want to join the violent agreement!

So to sum up –

  • Do not put in a catch all when designing the patterns.
  • Then, if need or whimsy takes you, add patterns that fill in your gaps up to and including a catch all.
  • For impossible cases it is useful to use
sys.error("unreachable code - Here be dragons.")

or some other exception with whatever message you like. Your impossible case today might not be so down the line.