-
Hey Paul, I recognize that we had some discussion around DUs in the past but wanted to formalize your opinions on the subject. I'm really struggling with the idea that no matter how I implement DUs in C#, there is always potential for a runtime error any time a new type is added to the union. I've tried both privately constructed abstract classes and the OneOf library, but to no avail. I prefer OneOf because it's able to tell me when I've got a case missing (and doesn't rely on a "catch-all" case that throws an exception), however the arguments are purely positional. This means if I add a new type to the union anywhere but the end of the union, I can have runtime errors without even being aware of them. For example: document.DU.Match(
raw => DocumentEntityType.RawDocument,
pdf => DocumentEntityType.PDFDocument,
csv => DocumentEntityType.CSVDocument,
pamData => DocumentEntityType.PAMDataDocument
); this fails unexpectedly if I add a type to the start of the DU, because there is no constraints on the types on the RHS. This can be solved with some compile-time trickery: document.DU.Match(
raw => {var _ = raw as RawDocument; return DocumentEntityType.RawDocument;},
pdf => {var _ = pdf as PDFDocument; return DocumentEntityType.PDFDocument;},
csv => {var _ = csv as CSVDocument; return DocumentEntityType.CSVDocument;},
pamData => {var _ = pamData as PAMDataDocument; return DocumentEntityType.PAMDataDocument;}
); This at LEAST gives me compile-time safety, but this is horrendous to write, and any attempts at turning this into a higher-order function just renders the compile-time capabilities of this useless, putting me back at square one with runtime exceptions. Is there any way to handle this in C#? Am I a lunatic for expecting 100% compile-time guarantees in C# like I do in Haskell? Where do I need to compromise, and what am I missing? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
If I could do this: document.DU.Match(
RawDocument: raw => DocumentEntityType.RawDocument,
PDFDocument: pdf => DocumentEntityType.PDFDocument,
CSVDocument: csv => DocumentEntityType.CSVDocument,
PAMDataDocument: pamData => DocumentEntityType.PAMDataDocument
); Then this would work. However the OneOf library offers no such thing. I'm wondering, is there a way I could fork this library and edit the Match function to work similar to how the Match function works on your Either type? Have you attempted this in the past and can you offer any advice here? |
Beta Was this translation helpful? Give feedback.
-
unfortunately, you're dealing with the fact that discriminated unions aren't a first class language feature and that's gonna be your lot until it appears in whatever version of C# in the future. You could take a look at my previous code-gen library which is now in deprecated, because one of the dependencies isn't supported anymore. If you're feeling adventurous, then you can pretty much take the code as-is (because it uses the Roslyn API) and drop it into a Source Generators project. It will generate the Match functions that you need as well as other useful API methods. https://github.com/louthy/language-ext/blob/v4-latest/LanguageExt.CodeGen/UnionGenerator.cs |
Beta Was this translation helpful? Give feedback.
-
I may have found an answer! 😄 It's a bit heavy in boilerplate but at least I can localize this boilerplate for the time-being, and it's safe at compile-time public abstract record Document
{
public Guid ID { get; init; }
public Guid LocatorID { get; init; }
public FileDetails FileDetails { get; init; }
protected Document(Guid id, Guid locatorId, FileDetails fileDetails)
{
ID = id;
LocatorID = locatorId;
FileDetails = fileDetails;
}
public abstract TResult Match<TResult>(
Func<RawDocument, TResult> RawDocument,
Func<PDFDocument, TResult> PDFDocument,
Func<CSVDocument, TResult> CSVDocument,
Func<PAMDataDocument, TResult> PAMDataDocument);
}
public sealed record RawDocument : Document
{
public byte[] FileData { get; init; }
private RawDocument Self { get; init; }
private RawDocument(Guid id, Guid locatorId, FileDetails fileDetails, byte[] fileData)
: base(id, locatorId, fileDetails)
{
FileData = fileData;
Self = this;
}
public static Either<Error, RawDocument> Create(Guid id, Guid locatorId, FileDetails fileDetails, byte[] fileData)
=> Right(new RawDocument(id, locatorId, fileDetails, fileData));
public override TResult Match<TResult>(
Func<RawDocument, TResult> RawDocument,
Func<PDFDocument, TResult> PDFDocument,
Func<CSVDocument, TResult> CSVDocument,
Func<PAMDataDocument, TResult> PAMDataDocument)
=> RawDocument(Self);
}
... |
Beta Was this translation helpful? Give feedback.
I may have found an answer! 😄 It's a bit heavy in boilerplate but at least I can localize this boilerplate for the time-being, and it's safe at compile-time