Ziglings: Learn the Zig programming language by fixing tiny broken programs
github.com
github.com
I've written a little over 1,000 lines of Zig at this point and I really like it. I think its key feature is a rich compile-time imperative programming environment ("comptime"). If you can have full compile-time imperative code execution, you can get a lot of the benefits of more complicated language features (like C++ templates) "for free."
In C++ templates are "a language within a language", but even templates with all their complexity cannot solve all of the problems you might want to solve at compile-time, so C++ has been gradually expanding its compile-time code execution features so that more and more of the language is available at compile-time (constexpr, consteval, etc). Zig jumps straight to the finish line by making most of the language available at compile-time from the outset, and in doing so avoids the need to add the complexity of templates in the first place.
Having "slices" as a first class type feels like a general and powerful solution to the problems that std::string_view and std::span are trying to solve.
I am comparing Zig to C++ a lot, which many Zig fans would probably take exception to as Zig does not aspire to be a better C++, but rather a better C. Indeed many key C++ patterns like RAII are explicitly out of scope. But to me Zig bridges the gap between C and C++ by solving many of the problems that make C feel too spartan without jumping to the incredible complexity of C++.
There are a few things about Zig that give me pause. I've noticed that compile-time feels like a very lazy environment, meaning that functions do not seem to undergo full semantic analysis unless they are called from somewhere. You can write a function that compiles successfully, leading you to believe the function is syntactically and semantically coherent, only to find that when you add an actual call to that function, the compiler now flags errors inside that function. This adds some friction to development, because the act of writing a function is no longer a self-contained activity. Writing the function feels more like sketching it; later when you actually call it you have a new set of compile errors to contend with.
I also miss tools like ASAN to catch memory errors. I'm guessing things like that will come with time.
Overall I feel very positive on Zig.
I was one of those people that started the idea that Zig should be compared to C more than C++. One day I'll express more clearly what I meant by that, but in the meantime I would say that Zig can and should be compared with C++ too.
More generally I think we got to the point where Zig deserves to be analyzed in it's own right and not as a reflection of another language because, among other things, it leads to this kind of misunderstanding:
> I've noticed that compile-time feels like a very lazy environment, meaning that functions do not seem to undergo full semantic analysis unless they are called from somewhere.
Comptime's lazyness is a necessary feature and not an accident. This is how Zig can avoid having a macro system.
More in general the idea is that you are supposed to write tests for your code, which will then ensure your functions get analyzed, and which in turn will produce documentation for your code. This makes testing more core to the development process than it is in other languages.
I'm not saying everyone has to like this approach, but if you stop at the comparison with C, you risk missing how the design of Zig is able to spiral into an new and radical programming experience.
See for example this Twitch clip from yesterday: https://clips.twitch.tv/RockyLivelyChimpanzeeDoubleRainbow-P...
I agree with your point because zig has evolved, but I do think that it is important to note at least for historical purposes, that zig was created literally as a "better c", and a lot of decisions are made because of some specific pain point X or Y in c
That is the role nothrow in placement new, and custom STL allocators as language tool.
Additionally plenty of APIs now have exception and error based versions with noexcept.
But even with exceptions enabled fallible allocation in practice does not work. There was an article that made malloc to return null randomly. All tested C++ implementations crashed then because they allocated memory when generating exceptions.
I am also curious to see how serious was that article.
You also skipped the section where Rust in its present state doesn't has any kind of recovery, it just panics.
Holy moving goalposts, batman!
The "don't pay..." thing has only ever applied to runtime, there nothing in there about not having to write more code yourself, etc.
(It's also a bit dubious in the first place. Chandler had a good talk about this at one of the CppCon's, IIRC. Can't be bothered to look it up, but it should be on YouTube.)
Can you elaborate on lazy analysis replacing the need for macros?
switch(build.target.os) {
.Linux => std.os.fork(),
.Windows => std.os.funcUniqueToWindows(),
else => @compileError("feature not supported for target os"),
}
This a simplified example to say that each path that depends on a comptime condition, such as the target OS, for example, feels intuitively consistent but in Zig types can (and do) mutate depending on those conditions and if the compiler were to eagerly check dead branches it would find plenty of semantical errors. In the stdlib you can see how `os` corresponds to a different struct definition depending on the target: https://github.com/ziglang/zig/blob/master/lib/std/os.zig#L5...I have some ideas to address this, but it does represent a flaw in the status quo design of the language.
I experimented a bit at how D's documentation generator behaves using these language constructs. Here's a snippet of some D code:
/// some struct description
struct Foo(T) // template
{
/// some alias description
alias Result = int;
/// some method description
Result foo()
{
string a = 3; // this does not normally compile
}
static if (is(T == int)) // evaluated at compile time
{
/// some method description 2
void bar() {}
}
version (Windows)
{
/// some method description for Windows
void bazWindows() {}
}
else version (Posix)
{
/// some method description for Posix
void bazPosix() {}
}
else
static assert(false, "Unsupported platform"); // evaluated at compile time
}
When generating documentation for the above code, and `Foo` has not been instantiated, the generated docs will include `Foo`, `Result`, `foo`, `bar`, and `bazWindows`. This is regardless of platform. The return type of `foo` will be `Result` and not `int`. This clearly shows that the D compiler doesn't perform semantic analysis when generating documentation. When doing a regular compilation and `Foo` is instantiated, `bar` will only be included if `T` is an `int`. `bazWindows` will only be compiled on Windows and `bazPosix` will only be compiled on Posix platforms.Looking at the implementation, the compiler will generate the docs after semantic analysis and only if there are no errors. But, if `Foo` is never instantiated no errors have occurred so it will continue to generate the docs.
On the other hand, if `Foo` is instantiated (and compiles) the compiler will generate docs for the AST after semantic analysis has been performed and `bazWindows` will only be included if the docs were generated on Windows and `bazPosix` will only be included on Posix platforms. What's weird though, is that it seems `bar` will be included regardless of what type `T` is.
With Zig you can have a function where some or all arguments are marked as "comptime", which means the values for those arguments must be known at compile time. Combined with the fact that types can be used as values at compile time means that you can use Zig to generate Zig functions in a safe way.
(I'm vaguely familar with Zig from a talk by the creator about 1½ years ago, fwiw.)
Macro's in Lisp are essentially "just" functions where the arguments are un-evaluated - you can use all the same functions etc...
Writing a simple Lisp interpreter is really quite educational - when you add macros and quoting into the language, you suddenly have a realisation at how simple it is.
(defmacro my-quote (x)
`(quote ,x))
and (defmacro my-quote (x)
(list 'quote x))
That `(quote ,x) is just way of writing (list 'quote x), not a notation for writing macros.That's it.
But on the other hand, even C++ templates are Turing complete and even C++ parse trees can depend on the output of a Turing-complete evaluation. So it is hard to see what benefit a C++-like template feature is buying in comparison.
My current work on a visual programming compatible language also uses this strategy for meta programming, so I’m very familiar with the academic literature. It certainly seems that Zig got this right and is doing well implementing it in a way that is usable and understandable.
MetaML certainly does seem to be similar to Zig, although the one paper I've now read thanks to your suggestion (https://www.sciencedirect.com/science/article/pii/S030439750...) does not mention introspection and type functions, and another commenter here suggests that it is, actually, referentially opaque.
1. It is referentially transparent, i.e., unlike Lisp, i.e. nothing in the language can distinguish between `1 + 2` and `3`. This means that the semantics of the language is the same as if everything was evaluated at runtime, and the comptime annotation has no impact on semantics (assuming syntax terms aren't objects and there's no `eval`).
2. It supports type introspection and construction.
3. It doesn't have generics and typeclasses as separate constructs but as applications of comptime.
I think 3 is unique to Zig, but I wonder about 1: is it possible in MetaOCaml, as it is in Lisp, C, C++, Rust and Haskell -- but not in Zig! (or, say, Java) -- to write a unit `m`, such that m(1 + 2) is different from m(3)?
One that comes to mind is Terra: http://terralang.org/
In retrospect Terra seems a clear precursor to Zig (though I don't know if it was a direct influence).
pub fn floor(x: anytype) @TypeOf(x) {
const T = @TypeOf(x);
return switch (T) {
f16 => floor16(x),
f32 => floor32(x),
f64 => floor64(x),
f128 => floor128(x),
else => @compileError("floor not implemented for " ++ @typeName(T)),
};
} template <class T> T floor(T x) { /* ... */ }
I think a documentation generator could display your Zig signature verbatim and a user could make sense of that.One alleged benefit of Java constrained genetics is that they allow in theory better error messages. Still in complex cases even after over 15 years with genetics in Java the error messages are not that great. Zig to some extend delegates to the compile-time library authors responsibility of producing good error messages. But then the error messages can be improved without waiting for the compiler to improve its reporting heuristics to cover all edge cases.
D has been making structs at compile time for years now, however.
struct Socket
{
version (Posix)
int handle;
else version (Windows)
SOCKET handle;
else
static assert(false, "Unsupported platform");
}
Another example is the checked numeric type [1] in the D standard library. It takes two types as parameters. The first being the underlying type and the second being a "hook" type. The hook type allows the user to decide what should happen in various error conditions, like overflow, divide by zero and so on. The implementation is written in a Design By Introspection style and inspects the hook type and adopts its implementation depending on what hooks it provides. If the hook type implements the hook for overflow, that hook will be executed, otherwise it falls back to some default behavior.[1] https://dlang.org/phobos/std_experimental_checkedint.html
This happens to me pretty frequently in C++. It won't compile with a syntax error, but if you don't call, say, a templated function, then the compiler simply can't know that the stuff you're doing inside is nonsense.
I don't consider this to be friction. I generally know what I've called, and what I haven't. I expect dark code to be bug-ridden placeholders with good intentions.
Mozilla Berlin used to host NodeSchool and the Rust Hack & Learn evenings. It became a bit of a hang spot, and at some point we had a pretty consistent group of folks who'd go to both. Marisa realized Rust could use something similar to NodeSchool, and started hacking on Rustlings during these evenings — which now a few years later has really taken off!
It's really cool to now see other languages in turn be inspired and follow from Rustlings ^^
I think this is the best way to learn languages.
Does anyone know of any for TypeScript?
Apparently someone did. ¯\_(ツ)_/¯ TIL
Opportunity for a index or site for koans.
(And that news story... what an absolute mess of a situation.)
I always get confused by all these new languages. Ive heard of: nim, rust, D, V, and now Zig. Where do I start? Should I start with C or C++? Or pick one of these? But which one?
Im currently looking at Lisp because I read it was good for alot of things. I'm also aware of Go and Swift but not looked into them at all.
Also, unlike the other languages you mentioned, Go and Swift aren't low-level languages. Low-level languages are those that give you very precise control over the machine instructions, almost as much as Assembly. Low-level programming is not as widely important as it used to be, because these days the cost of an instruction does not depend so much on what the instruction itself is but on the current micro-architecture state of the computer (e.g. which data is in which level of the cache; which branches are predicted and how) so even machine code is pretty high-level these days, but in some domains, especially memory-constrained environments, it is still irreplaceable.
By this definition I would say C is not “low-level”
I would say that a better approach in your case is to start by saying "I want to write programs to solve problems like XXXX", or "I want to learn a language that lands me a job doing YYYY".
For example, if you want to:
- build network services -> golang
- fiddle with OS level stuff (schedulers, memory management, etc.) -> C
- write high performance calculation/simulation programs -> julia
- get a nice solid base that can cover most of the above and has a promising future -> rust
Nim, D, V, Zig are very niche and I wouldn't recommend as a first "lower level" language (because you'll have a harder time finding answers, and they won't give you clear benefits over the other options in any problem space).
C++ is... huge. Modern C++ is a fine language, but you'll have to learn a lot to be able to handle it.
My main aim is really to learn about it. I wouldnt mind having a broad general experience in high level and low level before I decide to focus on anything in particular.
I've started looking a compilers and how they work im working through a book about building a compiler. Im really just interested in whats going on in the background. I always find myself wondering about the implementations of the languages I'm using.
C is not “low level”, in the assembly sense. It is absolutely a high level language. But it is the simplest, and the one with the most fine control.
Zig currently is able to target quite a bit. There's a good chance you could open an issue and it would get added, or find an existing issue and see where it is on the roadmap.
A nice way to discover a very promising language.
I'd love to hear if anyone's cross compiling to embedded chips in any real projects with it. Or it's strongest play still command line tools on Linux?
https://codelv.com/blog/2020/5/using-zig-and-the-stm32h7-to-...
https://www.mdeditor.tw/pl/2Ap9
Xtensa is less supported. M0 might also have some issues: https://github.com/Schroedingers-Hat/xtensa-zig
I've been meaning to try it out for embedded myself; I'll let you know how it goes.
The "Spaces vs. Tabs" argument shouldn't be what stops you from joining hands and working together :)
A great advantage of rustfmt and friends is that I don't really have to care about my editor being correctly configured, I can just code without worrying about style and run rustfmt before comitting. But if the compiler outright rejects poorly formatted code it means that I have to run the format tool every time I want to compile or risk having my code rejected due to format issues. Now I have to care about my style again! It's the opposite of what I want.
I mean it's a very small issue and mainly a bikeshed, but that seems like an odd decision to me.
If your project is small, just run `zig fmt` on the command line.
If you have a large codebase, you can just incorporate it into your build process (similar to how clang-format and clang-tidy are used in CMake projects).
But again, if you have a large codebase written in Zig, you've probably already configured your editor to run `zig fmt` on save :)
At this point it means you're probably Andrew himself because the Zig compiler is the only large Zig codebase out there;)
I'm solidly in the tabs camp myself I just understand bikeshedding about this class of issue isn't what zig wants to/should be worrying about at the moment while core components are still being shifted around and written. In the meantime "zig fmt" runs as part of my build and life moves on.
I used to fight about prettier (js) gofmt and so on. I just finally found I don't care. It's more fun to just code and watch it all get autoformatted to the project or language standard
This is simply not the case for any editor I have used recently.