What is the best way to handle pattern matching in the following situation?sealed trait Metadatafinal case class Metadata1() extends Metadatafinal case class Metadata2() extends Metadatafinal case class Metadata3() extends Metadatasealed trait Datafinal case class Data1() extends Datafinal case class Data2() extends Datafinal 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?
Pattern matching against two similar types
functional programming
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 * Data3Of 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.