I previously wrote about result types and union types in C#. I got to wondering if a union-based approach would allow nicer result types. In short, it works, but not as nicely as I’d hope.
Some of my previous goals for results types included
- Low-barrier to creating result types
- Alias result types to mitigate long generic type parameters
- Interoperability between result types, being able to map from one to another.
Basic Result Type
It’s pretty quick and easy to create a result type with a union approximation
|
|
This could then be consumed like
|
|
The main issue here is pattern exhaustiveness (the _
case). There is no way to limit the recognized derivatives to just Success and Error. Therefore we must always handle the open-ended case when pattern matching or surpress errors. This breaks my mental model of what I expect when matching on a result type. We can hide that detail by providing some .Handle(onSuccess, onFailure)
, but then we loose the desired benefit of pattern matching over callbacks.
Note that we can limit the actual possible derivatives. The Result<TSuccess, TError>
constructor can be made internal
to limit derivatives to the source assembly. We can even make the constructor to private
and still derive nested types, like how Success and Error are nested above.
It should be possible to improve the static analyzer to recognize limited derivatives based on constructor accessibility, but the current analyzer does not do so.
Aliasing / Deriving
C# does not allow type aliasing, so any named results have to be derived types of Result.
Deriving from our earlier result type leads to some issues. Mainly, Success
and Error
are Result<int,string>
and not valid instances of the derived type
|
|
This could be worked around in the class-based result-type experiment. However, a similar workaround doesn’t compile for the union-like approach. The success and failure types would need to inherit from arbitrary derivatives of Result, but generic type parameters cannot be inherited from.
|
|
Conclusion
Overall, making a result type using union-like records is pretty quick and easy. Unfortunately, pattern matching is a bit awkward and scenario-specific aliases cannot be derived from the root result type.