Hassle-free error management
Consider you write a function
fn div(a: u8, b: u8) -> u8 {
a / b
}
If later you find an error case that needs to be handled updating the code
usually will not require much extra addition. error DivideByZero;
fn div(a: u8, b: u8) -> %u8 {
if (b == 0) {
error.divideByZero
} else {
a / b
}
}
The caller can choose to ignore errors using `%%div(5, 1)` or can propagate
errors to the caller, similar to Rust's `try!`, `?` using `%return div(5, 2)`.I find this so easy that I'm much more inclined to think about edge cases and handle errors up front. I find when writing Rust the extra setup and management of errors adds a fair bit of tedium (although to be fair, with error-chain and proper setup at the beginning of a project this isn't too bad).
Compile-time programming
Zig has pretty strong compile-time programming support. For example, its printf formatting capability is all written in userland code [1]. It doesn't at this moment support code-generation like D's mixins but I personally have not found this too problematic.
Generic functions can be written in a duck-typing fashion. With compile-time assertions the inputs can be limited to what they need pretty clearly and the errors during usage are pretty self-explanatory.
error Overflow;
pub fn absInt(x: var) -> %@typeOf(x) {
const T = @typeOf(x);
comptime assert(@typeId(T) == builtin.TypeId.Int); // must pass an integer to absInt
comptime assert(T.is_signed); // must pass a signed integer to absInt
if (x == @minValue(@typeOf(x))) {
return error.Overflow;
} else {
@setDebugSafety(this, false);
return if (x < 0) -x else x;
}
}
Zig doesn't have any form of macros. Everything is done in the language itself.>I find this so easy that I'm much more inclined to think about edge cases and handle errors up front. I find when writing Rust the extra setup and management of errors adds a fair bit of tedium (although to be fair, with error-chain and proper setup at the beginning of a project this isn't too bad).
How is this different than say:
5_u8.checked_div(1).unwrap()
That unwrap() is performing the same thing as your %% example, if I understand it correctly.Are the Error types in Zig on the stack or the heap? By default Rust puts everything, including errors, on the stack, which means that the size of the return type always needs to be known. To make this easier you can return Boxed errors:
fn this_errors() -> Result<u32, Box<Error>> { ... }
And then error types can be very simple. Also, I recommend people getting into Rust really checkout error_chain!, which is a macro that helps in combining all the errors that your library might need to deal with: https://docs.rs/error-chain/0.11.0/error_chain/Error values under the hood are just unsigned integers and are returned on the stack. In fact, the granularity at which allocators are exposed in the stdlib makes any possible dynamic allocation very explicit in the language.
This link [1] provides an overview of errors and some of the surrounding control flow.
If you had an error you wished to pass a reason string for, would you need to use a global static or something?
If you wanted to send data you would need some other means like you suggest. I'd be interested in finding an ergonomic solution to this but it probably wouldn't be at the language level.
So we'll take your Zig and shove it through a preprocessor or several. I've seen code get preprocessed 3 times during builds, but I'm sure that isn't a record.
If a language designer fails to provide a suitable well-matched and effective preprocessor, we'll add something nasty. Oh well. Stuff has to get done.
technically the C preprocessor is a non-optional part of the C language and specification (and in fact embedded in the actual C compiler binary in many implementations). So both in theory and in practice C very much has a macros.
/pedantic
https://news.ycombinator.com/item?id=12378922
https://news.ycombinator.com/item?id=11060282
Actually I'm surprised that Zig didn't come up on the "Some Were Meant for C" thread, because this strategy is exactly what I wished something like Rust had support for (importing header files directly, more direct support for unit-by-unit translation, etc.):
https://github.com/rust-lang-nursery/rust-bindgen
It will translate a C header into Rust for all your bindings. Something could probably be built to do this more on the fly. If it doesn't already exist.
edit: docs link https://docs.rs/bindgen/0.30.0/bindgen/
edit2: and I should mention the inverse, Rust to C bindings,
If you look at the Python vs. Lua C API you can see that difference. Lua was designed to interoperate with C (albeit after breaking the language 4+ times); Python just took whatever their implementation happened to be, and then exposed that API to users. With thousands of functions and macros and GIL and whatnot.
Then a bunch of people came along later and wrote 5 different binding generators, and all of them solved some the problem, but not all of the problem. And some of them created new problems (SWIG).
Also (to child comment), is it really possible to do bindgen as a macro rather than as a separate code generating tool? Because I assume bindgen has to invoke a C++ parser like Clang.
I looked at Zig and it indeed uses libclang to parse C. I don't know about Rust's macro system but it would be somewhat surprising if it can invoke external tools or run C++ code at compile time.
They allow you to do much more. You can see this put to use in the Rocket project quite heavily. I believe it should be possible to link against bindgen as described and generate the bindings inline.
The caveat here is that it would be fairly opaque to the user as you wouldn't see the generated bindings in code. For situations like that I rely on 'cargo doc' to generate the documentation such that I can see the actual interface generated.
Rocket codegen for example: https://github.com/SergioBenitez/Rocket/tree/master/codegen
So it means that even if Rust had a standard C header include directive it still wouldn't be completely transparent because most of the time you want to wrap around C APIs in Rust to provide safe, borrow checked and pointer-free alternatives. There's a different philosophy I think.
Bindgen is often invoked in a "build script", which Cargo runs before building your project. So not exactly, but basically.
That's an interesting choice. I'm sure there are other semi-recent languages that have made the same choice, it would be interesting to hear the benefits and problems of that approach.
I say semi-recent, because there are of course many from before C or that competed with C and Unix initially, but unless they are still used much (Lisp?) it's not necessarily the best for a comparison of modern issues.
For zig it is not all or nothing of course. It is pretty easy to link and use c if wanted. The following for example asks the compiler to link against libc.
zig build_exe main.zig --library c
One other benefit that zig gets is more compile-time execution opportunity. The math library for example being written in zig means that we can determine `math.log(7)` and use it for compile calculations when using libm we couldn't. it would be interesting to hear the benefits and problems of
that approach.
A big problem is that libc interfaces are more stable, which means with libc you don't need to recompile as much when the system is upgraded, if ever.Solaris and Linux work hard to keep their kernel interfaces stable, so it's less of an issue there. But Apple, for example, very explicitly says that kernel interfaces (Unix syscalls and Mach ports) are unstable and unsupported, generally speaking. Consumable interfaces are provided via the userland runtime (e.g. libc). Notably, Apple's toolchains don't even support static compilation.
A system like OpenBSD isn't so gratuitous as Apple in terms of breaking compatibility, but because they're focused on improving and simplifying a cohesive system, they don't flinch about breaking compatibility when needed. For various reasons that happens more often with kernel interfaces than with libc interfaces.
Rust is the one (out of Rust, Nim and D) that looks most promising to me from the outside for my goals[1], but I haven't really settled on one to devote my very limited free time to.
1: If I'm going to drop down from a high level dynamic interpreted language to a low level strongly typed compiled one, I might as well got the extra distance Rust is asking for the gains it promises.
There's probably Rust ways to do that stuff, but it was not obvious to me.
Or, if you're like me and not confident enough to do this, you could check on crates.io[1] to see if no available lib already does what you need. In which case you basically have it for free.
I will give ziglang a try!