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.