The error payload thing isn't terrible. If you really really need it, it's not burdensome to add an error payload parameter pointer alongside as one of the parameters.
The error management was purposely build so you don't have to pass an additional parameters to know if your call failed or not. If you have to pass a parameter which will be then unused (null, undefined, etc) if no error was raised then you get a redundant mechanism.
I think the Zig team recognize this because they still have an old issue open for it which is still planned [0].
I'd really like something to help with that issue. I really appreciate Zig's error handling right up until you need to pass along any info other than essentially an error code. Then you have to throw away a lot of the syntax niceties and error tools that it gives you. I understand there's complications, though, with dynamic objects being returned up the stack, possibly requiring allocation, rather than a simple enum.
Rust gets this right for me, at a semantic level, where an Error is just an ADT with an arbitrary payload. However, while there's something to be said for consistency in a language and treating errors as any other values that you work with via `match` and any other ordinary language constructs, I really like that Zig has some syntactic sugar for working with it. Errors are so prevalent and necessary to deal with, they don't have to be exactly like ordinary values, and it's nice that, e.g., Zig has special handling for them in `if` statements, and ways of working with them in function signatures, and automatic error unions and such.
To be fair, Rust also comes with some specific `if` constructs to deal with error management and has the equivalent of zig's `try` with `?`.
I also like that their Result object has many methods to express how you want to specifically handle error case (all the variations of unwrap, unwrap_or, or, etc...).
test.zig:4:23: error: error is ignored
std.fmt.allocPrint(a, "", .{});
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~
test.zig:4:23: note: consider using 'try', 'catch', or 'if'
referenced by:
main: test.zig:12:9
callMain: /home/phil/vendor/zig-linux-x86_64-0.11.0-dev.2213+515e1c93e/lib/std/start.zig:617:32
remaining reference traces hidden; use '-freference-trace' to see all reference traces
To be honest, programming in Zig sounds like a PITA to me. Building proper abstractions is difficult, and especially so if the main goal of the language is to NOT let you do this.
And this is what low-level programming is by definition: technical details stand on your way to abstraction building. Memory, error handling, underlying machine details leaking, etc.
Zig tries very hard to stay (relatively) simple, make things possible, but not hide all the important details.
I don't mind technical details. But the best way to tackle technical details is to build the right abstractions. And the best way to build the right abstractions is to be aware of the technical details.
There are no "right abstractions". There are tradeoffs with advantages and disadvantages. Your preferred abstractions are by no means the "right" ones, especially since you don't even define what they are.
Of course there are right abstractions. And of course what the right abstraction is depends on the context, and takes into account the trade offs you are making. Any programming language is an abstraction, for example, and no programming language I am aware of is the right one out of the box except for very narrow and well-defined domains. That's why it is important that the language lets you build your own context-dependent abstractions.
And part of that context is your personal preference. Zig purposely chooses to prevent hidden control flow [1]. If operator overloading is a preference of yours, the language will get in your way. And that's fine. But it doesn't mean you can't build the "right abstraction" with it. These tradeoffs force you, for instance, to add an allocator to every type that needs dynamic allocation. For me that's great because it's evident which components need memory and I can make a conscious decision on what strategy to use with each one (or to avoid them entirely). For someone else, that might be a PITA. There is no "right" abstraction here.
No, your personal preference is not part of the context. Unfortunately, too many programmers think that way.
If you don't like using abstractions, but if you like instead to make everything explicit, that is of course your prerogative.
But denying that there is a right abstraction will just keep you from finding it. It doesn't change the fact that there often is one.
Going after the right abstraction takes time, and it might be just too expensive in your context, or your context may actually make abstraction infeasible. So in my opinion, it is all about trying to get into a position where experimenting with abstractions becomes cheap and economical. In principle, we should be in a much better position today in that respect than, say, 30 years ago.
But what makes an abstraction 'right'? I would say it's if it makes the program easier to make correct, or easier to write, or easier to understand, or easier to maintain, or ideally some combination of all of those. But easier for whom? Some programmers may have an easier time with more abstractions to provide guardrails and reduce redundancy; others may find it more effective to have everything laid out explicitly. And of course it's not just a question of more versus less abstraction; individual programmers often have specific abstractions and design patterns that they like to reuse in many contexts.
That's not quite the same thing as 'personal preference', where the right answer is by definition whatever I think is right. It's possible for me to think that XYZ abstraction or lack-of-abstraction works well for me, when in fact there's some other approach would that work better if I took the time to learn how it worked. Maybe I just haven't heard of that approach, or maybe I'm lazy or inexperienced or don't have the time to learn. People always think they're more unique than they really are; compare to the concept of "learning styles" which keeps being debunked.
Note then that there's an inherent conflict in programming language design space between introducing too many abstraction building tools and low-level details. It is very hard to make things truly orthogonal. E.g. memory and resource management tends to spill all over the code, with the other example being error handling.
And once a language designer adds ways (and a programmer culture) to hide things - it becomes very hard to follow the code.
Zig attacks this design space from a low abstraction and explicit flow point of view.
> Note then that there's an inherent conflict in programming language design space between introducing too many abstraction building tools and low-level details.
Best C++ code that deals with low level details is as least abstracted as possible.
One of the reasons C is used for writing operating systems kernels is that it doesn't hide details. Imagine Linux kernel written in OOP way with design patterns and SOLID principles. :)
Have you ever written low level code? There's constant hardcoded addresses you have to deal with which are in the spec. You can't really do BIOS, UEFI, Bootloder, Systems programming, ... without it. You wouldn't do a "magic number" and would make it a macro. But *SCREENBUFFER = 0;
Yes, my point being that the macro in this case is an "abstraction", and we were discussing GP's claim "all abstractions bad" (i.e., to quote them, low level code should be "as least abstracted as possible"). Was it really not clear what I meant?
I've actually encountered seasoned embedded devs advocating using the magic number in this very case. Never really understood why. It was some combination of "abstractions bad" and "I want to be able to check all adresses against my printed dead-tree documents while debugging "...
Of course noone really believes "all abstractions bad", but I wonder why some people still claim that.
Yes, I agree that there is this tension that no single "programming language" can resolve. That is why we don't need programming languages, but systems for building abstractions.
But as long as we don't have those systems, I prefer programming languages that let me build proper abstractions.
> But the best way to tackle technical details is to build the right abstractions.
Wrong way (according to you): Compute factorial using AVX intrinsics
Best way (according to you): Make a factory class AbstractMathFormulaCalculatorBuilderFactory to instantiate a builder class AbstractMathFormulaCalculatorBuilder which can build an AbstractMathFormulaCalculator, from which you derive a FactorialCalculator in which you use an AbstractMathFormulaExecutor, built with an AbstractMathFormulaExecutorBuilder, generated by AbstractMathFormulaExecutorBuilderFactory.
Ada, Modula-2, Mesa and Object Pascal already showed how to do it properly, with nice abstractions, in what concerns systems languages with manual memory management.
> Building proper abstractions is difficult, and especially so if the main goal of the language is to NOT let you do this.
reply
People should try to use Zig more like C and less like Python, Ruby or Java. If you miss abstract factories, builders, observers, visitors, decorators, maybe Zig isn't for you.
I don't miss those. We seem to have a different understanding of what abstraction is. It certainly isn't reusing some pre-canned concepts in all the wrong places.
I got the impression from the article that they actually didn't achieve what they wanted. They had to use a printf before returning the error, because they could not make that information part of the error.
Well right, Zig errors do not have a payload component because Zig is very strict about not having hidden allocations (or control flow) and recognizing the fact that allocations can fail.
You are, of course, free to write and use your own error type and handle it however you like, just as in C, but you won't get to use the special error handling syntax Zig has which relies on errors just being integers.
Or, as was done here, you can get the information you wanted to include with the error out in any number of other ways. You could have an error stack you push string pointers too, or use logging, etc. Zig just doesn't do that for you because, again, Zig philosophically doesn't allocate willy-nilly.
> Zig errors do not have a payload component because Zig is very strict about not having hidden allocations
That's likely not the reason. The above article links to a Github issue in which people have been discussing various proposals for handling payloads since 2019, and not a single time it was ruled out because of allocations as those could be handled the same way as anywhere else.
Not to mention in many situations an error payload wouldn't necessitate an allocation. For example just allowing a single integer as payload in a parser would already be a huge help. If the language's standard library won't even return a position for JSON parsing errors I'd consider that a problem. Imho it is a mistake to give errors special syntax features without any way of expanding them.
Zig has tagged unions. Functions can return things other than integers. I don’t see what’s so hidden about allowing unions to be returned as errors. It is all in the functions return type.
An error is an abstraction Zig has chosen to provide over an integer. Without discarding any part of their philosophy, they can make this custom abstraction a bit more useful by allowing tagged unions to be errors.
Not allocating has nothing to do with not having errors. You can have tagged union errors without any allocation. Rust has them.
It is my understanding indeed that Zig doesn't want you to build too many abstractions. I think that there is room in the design space for languages with few abstractions. That's also what the creators of Go intended, if I understand correctly, and that seems to work for many developers.
> Zig doesn't want you to build too many abstractions
I don't want to build too many either. I prefer to build just the right amount of abstractions. Sounds though like Zig makes that difficult, so at least for me, it is a hard pass.
It's a matter of perspective, I would argue that C++ makes it really hard to write the right amount, especially when you want to use overly-abstracted third party code that forces you into making a deal with the devil sometimes.
Yep, just like a professional barber's razor is dumbed down because it only has one blade instead of 6, a swiveling head and a battery-powered vibration function.
A professional barber's razor is flexible through its simplicity. Zig's errors are not flexible due to their separate syntax rules, which seems like the opposite of one sharp blade that handles anything.
If you want total control over your hardware, you're free to use machine code.
Is a type system a leash? I mean surely you want to interpret memory however you want without the language getting in the way of forcing you to use methods.
Are unique/shared pointers a leash? Or the borrow checker? They restrict what you can do with your objects because you're a poor programmer who can't code correctly. No true developer needs memory safety.
Are calling conventions a leash? Surely you know better how to manage the stack and registers and everyone else should listen to you and do it the way you prefer.
It's not me calling Go users idiots: it's one of the Go designers!
“The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language, but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.”