4

What is the best way to handle pattern matching in the following situation?

sealed trait Metadata
final case class Metadata1() extends Metadata
final case class Metadata2() extends Metadata
final case class Metadata3() extends Metadata

sealed trait Data
final case class Data1() extends Data
final case class Data2() extends Data
final case class Data3() extends Data

(metadata, data) match {
    case (Metadata1(), Data1()) =>
    case (Metadata2(), Data2()) =>
    case (Metadata3(), Data3()) =>
}

In this example, metadata would usually remain the same, but data may be updated frequently.

Normally a compiler would complain if there is a case that is not handled. However, situations where, for instance, metadata is Metadata1 and b is Data2 would be considered an error condition and should all be handled by the same error handler. If we add a catch all case then the compiler won't give any warnings if Metadata4 and Data4 are added. What is the best way to handle a situation like this?

5
  • Your example is in Scala so I tagged it as such. If your question is not Scala-specific but applies to all programming languages with pattern matching (e.g. also Haskell or F#), please revert the edit.
    – amon
    Commented Mar 29, 2015 at 12:59
  • Yeah. I was intending it to be more general than just Scala.
    – MI3Guy
    Commented Mar 29, 2015 at 13:11
  • 1
    You need to specify the "we don't care" situation in more detail. You know, much of the code marked 'this should never happen' sometimes executes.
    – 9000
    Commented Mar 29, 2015 at 14:16
  • I tried to add some clarification to that part. Let me know if it still needs more details.
    – MI3Guy
    Commented Mar 29, 2015 at 16:21
  • 2
    I suspect the problem is that the types are wrong in the first place, however without knowing more about them its hard to suggest an alternative
    – jk.
    Commented Apr 8, 2015 at 7:45

1 Answer 1

3

You call the types "similar", but they're only similar to you. Compiler knows nothing of this similarity, so you either need the compiler to learn about it, or alleviate it somehow. Here are some possible solutions that I would consider in this situation:

Bite the bullet and list all the cases (aka the quick one)

Before considering more involved possibilities, remember you can always simply list all the cases. You have m * n cases there, so it should be manageable up to 4 or maybe 5 cases each if you squint enough.

In F# you have something called active patterns which let you define a custom way to divide the domain you're matching against. You can use that to encapsulate the verbose matching part and reuse it in subsequent matches. Scala has extractors which supposedly are comparable to active patterns.

Certainly this doesn't scale well.

Add the missing type (aka the correct one)

The observation that MetadataX and DataX types are supposedly similar, but the compiler doesn't know about it, would imply that there's an intermediate type that's missing from the picture. You can introduce that type and it will simply make the entire matching problem disappear.

type DataSet =
    | DataSet1 of Metadata1 * Data1
    | DataSet2 of Metadata2 * Data2
    | DataSet3 of Metadata3 * Data3

Of course, whether that makes things easier depends on how you will construct the instances of that type. I guess that you might have all the Metadatas on hand, and it's only a matter of getting the right one for the Data you're handed, but I'd really need to know more of the picture here.

Break up the match (aka the pragmatic one)

If there's too many cases to handle and you can't add the missing type for some reason, you can always restructure the match expression. You'd need to extract some common logic into functions, and you'd lose some readability, but at least you get around the combinatorial explosion of cases and still keep the "case unhandled" warnings. The code is in F#, but you can treat it as pseudo-code (less verbose than Scala anyway):

match data with
| Data1 d ->
    match metadata with 
    | Metadata1 md -> (* the proper logic goes here *)
    | other        -> (* handle the unmatched types here using a common function *)
| Data2 d ->
    match metadata with 
    | Metadata2 md -> (* the proper logic goes here *)
    | other        -> (* handle the unmatched types here using a common function *)
(...)

With this, you still have the exhaustive match on the Data cases, and the nested matches give only a slight overhead to each case.

I imagine it could be made even nicer by refactoring the nested matches into functions that pick one of two continuations depending on a Boolean check whether Data and Metadata types fit together.

2
  • You're probably right about the second choice being better if possible. I could probably use that in some cases where I've run across this. Also, It occurred to me that you could collapse the third option into a single pattern match by wildcarding one of the values after the correct match.
    – MI3Guy
    Commented Apr 9, 2015 at 22:49
  • The missing type is absolutely the best answer, @MI3Guy. It offers the simplest, clearest, safest code.
    – itsbruce
    Commented Apr 13, 2015 at 10:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.