Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.Sign up
attribute enforcing explicit matching of DU cases #731
EnforceTotalMatch attribute for DU (naming TBD)
The safety around exhaustive pattern matching is one of the strong point of F# type system,
For F# users, discarding some cases of a DU in a match is always a fine balance between future changes and state of the code using the DU.
I'm sure many F# users face situations while adding cases to the DU or reviewing / adjusting the existing matches or those to add asking self:
In specific cases, it would be relevant to make usage of the wildcard discouraged more than morally, but with the tools of software engineers: the compiler.
type NormalDU = Normal1 | Normal2 | Normal3 [<EnforceTotalMatch>] type StrictDU = Enforced1 | Enforced2 | Enforced3 let test normal strict = let interesting = match normal with | Normal1 -> true | _ -> false let moreInteresting = match strict with | Enforced1 -> true | _ -> false // compile error (interesting, moreInteresting)
How To Fix:
NB: An error is produced opposed to the warning we usually get if there is no wildcard and missing cases.
Usage in other conditional constructs is an area of investigation / suggestions, maybe usage outside
This could be refined to apply to individual cases in a later revision, if the feature is relevant and doable.
Using this attribute could lead to a design time technique, where F# user would flip the attribute on while adding cases and removing it once all code changes and tests are done or to keep only in debug build; bringing some extra confidence of having reviewed all the matches.
Pros and Cons
The advantages of making this adjustment to F# are:
The disadvantages of making this adjustment to F# are:
Estimated cost (XS, S, M, L, XL, XXL): medium
Related suggestions: #414
Another con to this is that you can achieve this today by setting warnings as errors, though I can see someone wanting compile errors for failing to be exhaustive while not wanting an error on all warnings.
Some other considerations:
I'm in favor of this in theory, though I'd probably have to think about it more
Actually, I'm not able to achieve this today, if matches use wildcards, adding a case will result in warnings (that are great) in places there are no wildcards.
The design of that optional restriction feels similar to
Agreed, this is really a feature to allow optional strictness on specific DUs, and I don't believe it applies to general libraries, but more to domain specific code; where having some extra explicitness on how matching should be done for selected entities seems like a win / great tool.
Agreed, there would probably be need to enrich few places in the documentation, language specification, guildelines, maybe most of it could remain concentrated on the attribute documentation, and would go along well with existing content touching on
Thanks for feedback guys!
What about nested pattern matching on these types? Consider these examples:
[<EnforceTotalMatch>] type StrictDU = Enforced1 | Enforced2 | Enforced3 let strict = Enforced1 match Some strict with | None -> "nothing" | Some _ -> "something" // error? match [ strict ] with |  -> "empty" | _ -> "not empty" // error?
If the error happens for nested patterns, then I think it would be too impractical to use. If it doesn't happen on nested patterns, then the restriction quite easy to work around, limiting the usefulness of the feature. Maybe it would only actually be enforced if at least one case was explicitly mentioned?
@theprash great question!
I'm eager to see what other think about potential issues / inconsistencies that would make the feature useless or if it can be made sound / respecting the principle of least surprise for most F# users:
In both of the matches you shown, if the type of
If the compiler knows that
The second match is ambiguous and it would definitely be more expensive for the compiler to figure out if a wildcard is OK or should fail than it is now; since the second match you are using is not binding the DU instance at all, I think that one should succeed the compilation.
I agree that the second example is showing the kind of edge cases that would need to be nailed down, and more consistency/use cases considerations required for a RFC.
I don't think it should be let to happen; so long a
I think this can be implemented consistently if, for the purpose of warnings or compilation,
So in @theprash 's example,
match Some strict with | None -> "nothing" | Some _ -> "something" // Error: _ is not allowed to match a StrictDU match [ strict ] with |  -> "empty" | _ -> "not empty" // OK: _ is allowed to match a List<StrictDU> because List<_> does not have [<EnforceTotalMatch>]
In codebases that you control, I think advice to minimize use of
I think there's a big difference between a
match Some strict with | None -> "nothing" | Some _ -> "something" // matches everything
match Some strict with | None -> "nothing" | Some Enforced1 -> "something1" | Some _ -> "something2" // matches remaining cases
To me, the first case is just a general ignore and should not cause an error. The second one is the case where an error would be useful.
Great discussion. Thinking about this at a bit more of a high level, there are two axes by which to grade a feature like this: