I think part of the answer is that I haven’t thought that much about it, it felt like there might be an idea here worth exploring, but I couldn’t tell what the details would look like by trying to imagine all the implications, so I decided to explore it with code.
Another part is probably that I had seen this odin vs zig video before:
Does it? To me this seems similar:
const node_tuple = self.parseNumbers();
// have to pry open node_tuple to get to data
const node_result = self.parseNumbersMadeUpUnionVersion();
// have to pry open node_result to get to data
I think the one benefit of the latter might be if you always unpack the thing with switch statements. The things I like about the former are that you can easily say what is the result and what is the error, instead of the result just being one of the n cases. Also I like the idea of potentially having many different functions being able to use the same type for the error part.
Other things I wonder but haven’t actually looked into are:
- does the compiler do clever things for tuples being returned from functions, like treat them as if they were independent output parameters and optimize them individually, if that may be helpful?
- what happens when I have a really big error type but a tiny success result or the other way around, can the tuple be optimized better?
- if I “disable” these custom errors (reduce them to wrappers of zig errors the example has a flag -Dcustomerrors=false) can I get rid of the overhead of those errors, or close to it? Would this be more difficult if it was all one union, or just different?
- are there cases where you can write error handling functions that can be used for multiple functions that return that error type (where having the success result be mixed into the union would prevent reuse?)
Another thing, combining error unions makes sense to me, but how do I combine two things where one of the values isn’t an error but the success thing. Then when I want to convert back to the zig error I need to have some kind of convention / flag, that tells me how to get the error code or succeed. I think in a way you could argue that putting everything in one union is worse, because it puts everything in the same code path, instead of putting special attention on what you are doing with the error, but I am not sure if that is a strong argument, on the flip side you could say, that the switch at least complains about unhandled cases and if you use it for both data and error, you are more likely to handle the error. So I don’t know.
Seems to me without language support for these errors, you can’t really prevent someone from forgetting about an error (sure there are unused variable errors, but if someones lsp just fixups away that error, you could forget about handling it).
Another thing I wanted to explore eventually is whether you can build up more complex error types as you go up the callstack and collect more information, for that it seemed it might be easier to take apart and reassemble that 2-tuple than having to deal with something more “complex”/structured.
But I also kind of like the pattern shown in the github issue, with just creating a struct on the callsite and passing a reference to it via a config argument, sure it seems very adhoc, but I also like its simplicity.