From the viewpoint of someone learning about a new language, I find the accessibility of the standard libraries goes a long way toward helping me understand how things fit together. It is a first stop to see how experts in the language use it. Browsing through the standard libraries of languages like Zig, Go, and Python - they're usually well-documented and readable enough to be a tutorial, even before you've dug into learning it. Others (Rust, C++) are a bit more, ah, technical for the novice.
That's absolutely true, but (the standard library aside) the "syntax" -- or, rather the syntax and core semantics -- of a programming language are arbitrary axiomatic rules, while everything else is derivable from those axioms. So while it is true that a small language can lead to a not-necessarily-easy-to-learn overall programming experience, it is the only arbitrary part, and so the only part you need to memorise (or consult the documentation for when dealing with some subtlety you may have forgotten). So a smaller language reduces the need for "language lawyering" after you learn it.
Some languages (e.g. lisps) are deceptively small by relying on macros that form a "second-order" language that interacts with the "first-order" language, but Zig doesn't have that. It has only one language level, which is small and easy to memorise.
But yes, Zig is a bigger language than C, but a far smaller language than C++. What's amazing, though, is that C++ is strictly more expressive than C (i.e. there are programs that could grow exponentially faster in C than in C++, at least without the help of C macros), but Zig is as expressive as C++ (i.e. program sizes may differ by no more than a small constant factor) while being much closer to C in size, and it achieves that without the use of macros.
> Easy is subjective. But simple things are simple because they do not complicate; they have fewer concepts. Simple is objective.
I'd argue both are subjective, or with a better word: relative.
I'd say Zig is easier and simpler than C in almost all regards. The article does not specify what Zig is compared to in order to make these statements. Probably the author had C in mind, but that was not explicitly mentioned.
I thought about trying it out in as small data engineering project, but I'm not sure if language support is sufficient for the kind of tooling I would need eg. Database adapters.
The language itself is fun. The explicit-ness of choosing how allocation is done feels novel, and comptime is a clean solution for problems that are ugly in most languages.
Aside from lack of community I’d say the biggest nuisance is error handling in nearly everything including allocations. I get that allocation can fail but the vast majority of programs I write, I just want to panic on an allocation failure (granted these aren’t production programs…)
Edit: in retrospect, allocator error handling verbosity is probably necessary due to the power the language gives you in choosing allocators. If I use the general purpose allocator then my edge case is PC out of memory; if I use a fixed buffer allocator, a failed allocation is a code bug or a common error case, so we might want to handle the failed allocation and recover
Zig has comptime, which is essentially a compile time macro written in the main language and with type reflection capabilities.
They can introduce complex behaviour and fail in very cryptic and unexpected ways, which results in an experience very similar to macros or C++ template literals.
So if you want to encode matrix multiplication, then you'll always have to write `mat1 #* mat2`. This feels like a hack, and isn't all that elegant, but it'd be clear that every usage of such an operator is a disguised function call. (And according to what Andrew Kelley said, it's all about not hiding function calls or complex operations in seemingly innocent 'overloaded' symbols.)
If you want to take this one step further you'd probably have to allow users to define infix functions, which would be its own can of worms.
Honestly, I am not particularly happy with any of these ideas, but I can't think of anything better either!
This should be printed out as a large poster in every office.
It took a bit of getting used to when I joined but we agreed as a team to have all meaningful scripts written in Zig not bash (for one, bash doesn't work on Windows without WSL and we need to support Windows builds/testing/etc.).
It makes about as much sense as any other cross-platform scripting option once I got used to it!
Some examples:
Docs generation: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...
Integration testing sample code: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...
Running a command wrapped in a TigerBeetle server run: https://github.com/tigerbeetledb/tigerbeetle/blob/main/src/c...
In other words, you're forced to use the Zig Discord server if you want to find answers to any simple questions, and this is (sadly) not obvious at all to newcomers to the language.
It might not be a _real_ use-case or anything, but writing a `build.zig` file for an existing C project might be a good way to at least dip your toe in the water.
This is C interop.
I work with C quite a bit and I enjoy it, however writing a large project in C can be tiresome.
Having an option like Zig which can import C headers and call C functions without bindings is pretty attractive, especially when you want to write something a big larger but still stay in C world.
Some insiders underestimate the effort required for newcomers to build non-trivial things. I think this is because some of that complexity has to do with things like poor documentation, inconsistent stdlib, incompatible releases, slow release cycle, lack of package manager, etc. For an insider living and breathing Zig, not only aren't these huge challenges, they aren't really "Zig" - they are just transient growing pains. For someone getting started though, the current state of Zig is Zig.
I wish Zig had a polished package manager (there's one in the current development branch, but you don't as much use it as fight it). They could then move some of the less polished code into official experimental packages, helping to set expectations and maybe focus the development efforts.
There is also another interesting difference, albeit a theoretical one. Zig's comptime is what's known in formal languages to be referentially transparent (it basically means that you cannot distinguish between two otherwise identical objects that differ only in their reference name) while macros are not. Because referential transparency is strictly less expressive than "opacity" (but it's also simpler!), it's surprising that so many practical use cases for macros can be addressed with the less powerful (but simpler and much easier to debug) comptime. That's quite a discovery in language design. While other languages also have comptime-like constructs, they also have other complex features that have made it hard to see just how powerful something like comptime alone can be.
But I wish they added:
1. Ability to declare completely pure functions that have no loops except those which can be evaluated at compile time. Something with similar constraints as eBPF in other words. These could be useful in many contexts. 2. Ability to explicitly import overloaded operators, that can only be implemented using these pure guaranteed-to-finish-in-a-fixed-number-of-cycles functions.
Then you'd get operator overloading that can be used to implement any kind of mathematical function, but not all kinds of crazy DSL-like stuff which is outside the scope of Zig (I have nothing against that, I've done crazy stuff in Ruby myself, but it's not suitable for Zig)
Personally I've never had an issue reading through the source for zig std, and if your editor supports it you can just 'go to implementation' on most things. Hopefully the code remains relatively readable since I find it preferable to see the actual code + some basic tests rather than trying to navigate those documentation sites.
Or D, Nim, Swift, OCaml, Haskell, AOT C#, AOT Java,...
If any kind of automatic memory isn't possible/allowed, I would be reaching out for Rust, not a language that still allows for use-after-free errors.
Maybe it is a good language for those that would like to have Modula-2 with a C like syntax, and metaprogramming capabilities, and are ex-Objective-C developers.
There's a lot to like with Zig, despite its unconvential syntax and some language decisions I personally disagree with. The language is still in development but it's very promising and I'll definitely try to learn it before it reaches that magical 1.0 release.
[0] https://marketplace.visualstudio.com/items?itemName=vadimcn.... [1] https://marketplace.visualstudio.com/items?itemName=ms-vscod... [2] https://marketplace.visualstudio.com/items?itemName=ziglang....
So I would ask you this: what portion of your program suffers from a lack of user-defined infix operators and how big of a problem is it overall? Even if it turns out that the problem is worth fixing in the language, it often makes sense to wait some years and then prioritise the various problems that have been reported. Zig's simplicity and its no-overload (not just operator overloads!) single-dispatch is among its greatest features, and meant to be one of its greatest draws.
What are some other examples of such languages that rely on second-order languages? You mentioned Lisps. Would Forth be another example? Are there more examples?
I have come to the same conclusion but then I also fear that Rust will continue to expand in scope and become a monster language like C++. Do you or anyone fear that? Is that a possibility?
Now, it's definitely neat that you can do reasoning from first principles on it, but I'm not sure how much of a gain that is.
But mostly the team decided to do this because we wanted to unify on one language and double down on the investment in Zig.
I'm not a fanboy (nothing wrong if anyone is, just clarifying about myself); I think this choice was right.
-Dtarget=x86_64-linux-gnu.2.9
describe "order" do
it "is marked as complete" do
expect(@order).to be_complete
end
it "is not yet shipped" do
expect(@order).not_to be_shipped
end
end
It is a bit unfortunate because all of the above is a pretty tall order. We're getting to the point that new languages are expected to boil the ocean by the time they reach 1.0
Considering all that, I still see Rust as the most sane choice for writing native programs. I don't really want a "better C", I want to write memory-safe software with confidence, and that means static analysis or a thick safe runtime, whatever is more suitable for the use case.
Will it turn into a C++-like monster? I don't know. Maybe, but when it comes to C++ it always feels like its "monster" status is largely a result of previous mistakes or early design decisions piling up, and causing trouble (eg. C-style arrays, exceptions, implicit conversions, fiddly sum types rather than first class support, no pattern matching etc.).
Rust will grow large, and probably complicated, but the real issue with C++ are all these past mistakes and a shaky foundation, and Rust is doing much, much better in that regard. Time will tell if we'll eventually look back some of Rust's design decisions as bad or unwieldy (probably).
For C to Zig there’s plenty of reasons one might prefer Zig. For memory safety obviously you might opt to choose neither.
Right now I'm considering the same thing but with Odin and for many of the same reasons that I had for Zig; it's an excellent language to write foundational code in and once you do, you end up being able to build significantly more understandable, reliable, stable and consistent things.
This, but on a company level, is a real multiplier. Once you adopt a couple of Python scripts and that's OK, you give up the possibility of wielding this sharp spear.
Edit:
I think "How I program C" by Eskil Steenberg is an interesting window into what you can get if you laser focus on a language and environment and allow yourself to build up a mountain of code that you dogfood: https://www.youtube.com/watch?v=443UNeGrFoM
At some point I will likely soft-retire and at that point it's exceedingly unlikely that I'll bother with using other people's libraries except a few key ones that I think are decent, and at that point there really is no reason I'd sit down and write these fundamentals in anything but a lower-level language. Odin, Zig or something like it would pretty much be the only thing on the table.
I've seen some put it as "zig is good when `unsafe` heavy code".
Personally, even when writing life-or-death software, allocation errors were too much of a pain to deal with and much prefer Rust's approach for 99% of software. The question is if another language like zig provides enough value to justify existing for that 1% of use cases (all numbers made up :) ).
I'm not so sure a package manager is really all that essential; it can certainly be convenient but especially in the space Zig is looking at it's pretty workable without one (without complex deep dependency trees you can use git submodules or just copy a directory). Or let me put it this way: I never really missed a package manager in Zig.
The C strategy for this was just to wrap malloc() with something called xmalloc or malloc_nofail or whatever:
void *malloc_nofail(...) {
void *res = malloc(...);
if (res == NULL) {
abort();
}
return res;
}
(Or whatever.) The same would work in Zig, I think.This removes the need for operator overloading for a vector type, which covers most use cases of operator overloading and I often in fact think is the only legitimate use case.
I don't get to use `*` for matrix multiplication, but I have found I do not mind using a function for this.
I have only been playing with this in small toy programs that don't to much serious linear algebra and I haven't looked at the asm I am generating with this approach, but I have been enjoying it so far!
> An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior.
^ that is the definition of referential transparency I am aware of.
You seem to be implying that FPers have bastardized the term through their misunderstanding.
But the bog standard FP definition is a real and useful concept. Maybe it stole something else's name? But I don't think it's due to being mistaken. Because the FP concept itself is pretty rigorous.
There are also several similarities: Custom allocators as part of the ecosystem and language, no RAII, easy ways to propagate and handle errors the right way, tagged unions with table stakes like exhaustiveness checking.
I think Odin and Zig have some fundamental differences (and plenty of similarities) and when trying Odin out I was surprised to find that I preferred the Odin way overall.
For gamedev stuff Odin wins due to a few things; swizzling on a code level is super nice, array programming built-in to the language, vendor libraries shipped with the language that allow you to just get going almost no matter what you're doing, and so on.
For open source software (libraries or programs, that depend on other libraries or programs), it is essential (if you're not distributing single functions like with Unison). For closed source it doesn't matter that much.
it's nice when a new language has a package manager right out of the gate, but I would like to see more new languages take a more measured approach and aim to significantly improve upon past efforts, instead of merely replicating them out of some sense of obligation.
I really dislike how Discord has become “forums in the 2020s”
Compiling it requires the latest llvm toolchian (16), which is only realistically going to be available as a package if you're on a bleeding edge distribution.
Obviously a package manager is useful, but Zig is relatively low-level and long dependency chains are much less common than in e.g. Python, Ruby, and of course NodeJS. So I'd argue it's not essential. All the other things mentioned in the to top comment are far bigger issues IMO.
const std = @import("std");
const expect = std.testing.expect;
test "for basics" {
const items = [_]i32 {4,5,3,4,0};
var sum: i32 = 0;
for(items, 0.. ) |value,_| {
sum += value;
}
try expect(sum == 16);
}
ubuntu 22.04, snap zig version 0.10.1 ,
zig test 1.zig produces:
1.zig:7:12: error: expected ')', found ','
for(items, 0.. ) |value,_| {
^
Edited: To update, I tried snap, but using
snap install --classic --beta zig is a security risk because it can change the system and is not sandboxed.That's more work than apt-get install for sure, but not so much more
https://www.godbolt.org/z/7zbxnncv6
...maybe one day there will also be a @Matrix builtin.
Agreed. Especially on a formatter. The number of code review comments it cuts out is incredibly time and energy saving.
> It is a bit unfortunate because all of the above is a pretty tall order. We're getting to the point that new languages are expected to boil the ocean by the time they reach 1.0
In order to get into production? yes. there are minimum requirements that must be met. those are higher now than in the past, because of lessons learned the hard way. those problems have been solved (for some value of solved) in existing ecosystems -- a new language wont change the need for them to be solved.
it shatters the dream of hacking up something on a weekend and having it turn into a hit success, but it also removes the case of hacking up something in 10 days and suffering the consequences for the next 30 years.
Until they have what you mentioned, the languages aren't ready for large scale use -- they need to grow into it. They can be useful prior to that -- enthusiasts and early adopters can reap what benefits are provided. That adoption is what fuels the development of things like a standard code formatter.
edit: fixed omission of a unit of time after '30'
I just say nuisance because zig code often looks like Go code in that almost every return type becomes an error union as allocation error handling (and maybe other errors) trickle up
It's much easier for most people to download a standalone "mac" or "windows" binary than to know if they already have the right version of Python or Perl or Clang (and all the transitive dependencies your project adds).
Not quite. A referentially transparent expression (E) is one where you can replace any of its subexpressions (A) with another (B) that has the same meaning (not value!!!!) as (A) without changing the meaning (not value!!!) of E. However, in purely functional languages, the meaning of any expression is a value, but that's the important thing about them, not the fact that they're referentially transparent as imperative languages equally are. We often use the word "semantics" or "denotation" instead of "meaning" in the above, and we say that a pure functional one is one that has "value semantics", i.e. one where the meaning of an expression is a value.
Most programming languages are referentially transparent when not using macros (that was the whole point of talking about referential transparency in programming in the first place), and that's important because it demonstrates both the expressive power and the complexity of macros.
But yeah it is quite simple still.
Hopefully, this process will continue soon as the bug seems to be getting a bit stale.
Regardless, point taken. You’re more or less describing the internal fear I have (mentioned in another thread) that if I dedicate time to a nontrivial Zig project I will regret it if/when there’s no Zig community in N years
Lifetimes are painful, and a other languages are exploring better ergonomics or combining automatic memory management with affine/linear types, yet it was Rust's adoption that pushed other language designers to actually look into this. So from that point of view, quite a success, even if Rust vanishes tomorrow.
The more peripheral crap I have to deal with to use your language, the less I'm likely to use it in the first place. I don't need, want or care to learn yet another idiosyncratic fragile system. Finding source tarballs is a complete non-issue, and inevitably I'm going to have to manually build things anyways to figure out what erroneous assumption a library is making about the underlying system which is causing the build to fail or the runtime to crash, so the package manager just ends up being extra steps. Without fail, that has always been my experience with language package managers.
In the pursuit of making things simpler, we're really just making them harder.
Common Lisp user here. Am I missing anything that Zig has in the compile time department?
edit: Also, does the code in the comptime block have to be valid Zig?
https://www.godbolt.org/z/v8Ta8hEbv
Zig is exactly the kind of language where you'd want to build a performance-oriented primitive like that, but AFAICT the language doesn't let you do it ergonomically.
1. Easy Build System 2. Drop in Replacement for c/c++ compiler (zig cc) 3. Easy cross compliation 4. Single executable compiler 5. Package manager 6. Debugabillity
It's the sort of developer focus that has a huge impact especially when you are doing it for more than just a hobby. Crystal, another language I love, is weak in this area. No tree sitter, No tier1 windows support, Major libraries like the Lucky Framework that don't build on Windows, a buggy LSP etc. Don't get me wrong I love the language, and I will continue to work in it, and all of these things are in various states of progress, but it's already at 1.8 and some of these issues haven't been ironed out yet. I'm not just armchair complaining, I know these are hard, and I will contribute once I am more familiar with the language
For what it's worth I haven't found any language constructs that seem to make the compile time grow considerably, so I think the risk of adding a library and suddenly being faced with massive compile times is fairly low in comparison to some languages. With that said, I'm only using the core lib and vendor libraries.
JangaFX by way of GingerBill reports that their 200kloc EmberGen[0] project takes 5 seconds to compile:
> On my old machine, it took 5 seconds, and now it takes 2.2 seconds on my new machine.
Before some paths in the compiler had multithreading added to them that number was 15 seconds for the same project. As far as I know both of these numbers are for unoptimized builds, i.e. development builds.
@Matrix makes less sense because when it gets big, where are you getting memory from?
And those operators wouldn't have any precedence.
> If you want to take this one step further you'd probably have to allow users to define infix functions, which would be its own can of worms.
As long as these infix function are preceded by a recognizable operator ("#" in your example), I think that this would be fine.
It's like `apt` or `brew` for system dependency management. It is there if you want it but you can just as well download a tar and config/make/install yourself if you want.
In many ways, it is like a standard lib. No one forces you to use it. If you prefer the simplicity of your own implementations then I see no reason why you can't just write your own.
But when you want the advantages of a package manager, and there are advantages that you may not appreciate but others do, then having a standard one built into the language feels preferable to having a dozen non-standard independently competing variations that the community cobbles together.
Zig 0.10.1:
for(items) |value| {}
for(items) |value, index| {}
Zig 0.11-dev: for(items) |value| {}
for(items, 0..) |value, index| {}
for(as, bs, cs, ds, 0..) |a, b, c, d, index| {}
It's the sort of thing that lets a data structure be used on both a gpu and cpu, allocation out of shared memory, or ensure that no dynamic allocations are happening at all.
Most programs don't have those concerns - so zig may not be the best choice for them. For the programs that do have those concerns, forethought about allocators is pretty important. Right tool for the job, and all that.
vec2..4 and matching matrix types up to 4x4 is basically also what's provided in GPU shading languages as primitive types, and personally I would prefer such a set of "SIMD-y" primitive types for Zig (maybe a bit more luxurious than @Vector, e.g. with things like component swizzling syntax - basically what this Clang extension offers: https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...).
#{
m3 = m1 * m2 + m3;
m3 += m4;
}
Basically, pure syntactic sugar to help the author express intent without having to add a bunch of line-chatter.Speaking of operator-overloading, I really wish C++ (anyone!) had a `.` prefix for operator-overloading which basically says "this is more arguments for the highest-precedence operator in the current expression:
a := b * c .+ d;
Which translates to: a := fma(b, c, d)
Yes: https://github.com/ncaq/debug-trace-var The trick, however, is not unsafePerformIO (destructive mutability has nothing to do with referential transparency in general, although it breaks it in Haskell specifically) but with TemplateHaskell, as quoting has everything to do with referential transparency.
> But the bog standard FP definition is a real and useful concept.
Actually, it's rather tautological. It defines FP circularly (see my comment here: https://news.ycombinator.com/item?id=36152488). It says nothing more than the far more useful explanation: "the meaning of every expression is a value".
I'm assuming any pure language with macros is also r.t. at each stage and only pedantically breaks r.t. when combined. But I don't think that especially hurts the ability to do fast and loose reasoning so long as the core language is pure.
It definitely doesn't seem correct to say Java is more referentially transparent than Haskell here. You don't have to go into such niches in Java to lose that property.
But even pedantry can't argue that Java is a fundamentally more referentially transparent language than Haskell lol. That threw me for a loop.
> Something that makes Zig harder to learn up front, but easier in the long run is lack of undefined behavior.
Reminds me of the old discussions of Fortran Vs C, and specifically in the early times before C had a standard library. What we call "undefined behaviour" was just an idiom of the language where the "behaviour" was sometimes on purpose, but recognised might not be portable. And so the point here is the idea of undefined behaviour is tied to portability on some levels, and isn't just some purely academic idea about the compiler or abstract syntax trees.
So I'm concerned about the potential over-zealous prejudice against undefined behaviour, but I think we can all agree deterministic software is better than otherwise. The catch is that sometimes UB is deterministic, and yet dares to not be idiomatic.
A better measure IMO is how long it might take you specifically. For example, Rust is much easier for me to learn than Haskell because I have never coded in a functional language before. Golang was very easy to pick up on. I never took time to learn python and powershell, I just kept referencing existing code and googling for examples because they were both mostly familiar languages with a different syntax.
In other words saying they learned something fast or that something is easy for them, is at least partially self-promotion. So claims of how easy something was for them it have to be taken with a grain of salt.
The interesting thing for the rest of us is WHY something is easy, or difficult. That would be helpful for others to know. This article is just that, it tells us why Zig can be difficult. What to look out for.
Ha, I thought this sounded distressingly familiar!
"In September 1995, a Netscape programmer named Brandan Eich developed a new scripting language in just 10 days. It was originally named Mocha, but quickly became known as LiveScript and, later, JavaScript."
The reason is that there are existing high-quality languages/ecosystems.
(Which is a good thing!)
I don’t have much game dev experience though outside of simple games using libraries like raylib to just move and draw stuff. Maybe once things get complicated enough they are all like bevy.
In C++ branches that don't have to "compile" are explicitly marked as such with `if constexpr`. In Rust you've got macros that are also explicit with required `!`. In Zig any branch can be a ticking code rot time bomb, just like it was with C #ifdefs. It's better in Zig in that it at least has to parse and so you've got fewer blatant problems, but the category of problems of "I flipped DEBUG from `false` to `true` and now I have dozens of build errors to go fix" is still there. And you really don't have any warning signs other than "this is branching on something that might be known at compile time"
But honestly that's also my general impression of Zig as a whole. It's very definitely a "better C", not something that's necessarily good/competitive with the broader state of the ecosystem. It's like a love letter to C, completely with many of the same general problems that C is known to have and that other languages have addressed.
You've dramatically overstated your case, since that's true of every Lisp-like language.
Lisp is a perfectly suitable language for developing mathematics in, see SICM [0] for details.
If you want to see SICM in action, the Emmy Computer Algebra System [1] [2] [3] [4] is a Clojure project that ported SICM to both Clojure and Clerk notebooks (like Jupyter notebooks, but better for programmers).
[0] https://mitpress.mit.edu/9780262028967/structure-and-interpr...
[1] Emmy project: https://emmy.mentat.org/
[2] Emmy source code: https://github.com/mentat-collective/emmy
[3] Emmy implementation talk (2017): "Physics in Clojure" https://www.youtube.com/watch?v=7PoajCqNKpg
[4] Emmy notebooks talk (2023): "Emmy: Moldable Physics and Lispy Microworlds": https://www.youtube.com/watch?v=B9kqD8vBuwU
It's not so easy. You'd have to examine debugging information in stack traces and use reflection. You can't write such a "trace" operator in Java or in Zig. Of course, without macros, C is almost perfectly referentially transparent and Haskell is, too (except for unsafePerformIO).
For example, single player video games. You can exploit your own machine if you want, but that’s not an issue.
I like rust, but if I ran into async issues and annoying stuff, I could see a world where I grab a non-memory safe language to make games easily.
In most programming languages the reference or meaning of a term is not a value; in pure functional languages the meaning is a value and that's what makes them special, not their referential transparency which they share with imperative languages.
Here's an example from C:
int global_x = 0;
void f() { x++; }
void g() { x++; }
f and g have the same meaning in C (but the function `void h() { x += 2; }` does not) yet `m(f)` and `m(g)` will not have the same meaning if M is defined as: #define m(x) #x
However, f and g are interchangeable anywhere else (this is not actually true because their addresses can be obtained and compared; showing that a C-like language retains its referential transparency despite the existence of so-called l-values was the point of what I think is the first paper to introduce the notion referential transparency to the study of programming languages: https://github.com/papers-we-love/papers-we-love/blob/main/l... You may be surprised to see that Strachey also uses the word "value" but his point later is that value is not what you think it is)https://ziglearn.org/chapter-4/ See here
In a lot of ways, Zig is to the systems programming space what Clojure is to the information processing space.
If you have 5+ years experience, you want simple, not easy. Both languages deliver real advances on that promise in their respective domains.
----
Back to the article, perhaps not coincidentally, the product my startup is developing is written primarily in Zig (language runtime), Clojure, and an in-house Forth/Factor/Joy derivative for user-level scripting. I guess I really do care about simplicity…
[0] Simple Made Easy: https://www.youtube.com/watch?v=SxdOUGdseq4
Pip used to store packages in a global location, now most of python used a Virtual Environment per project.
Node uses a “vendor” directory within the project structure. This is probably the easiest case.
Go used to store everything globally in the “go path” but now with go modules it does something else.
Java doesn’t need source code, just single JAR files, but it needs it within the classpath.
C/++ is very flexible, but varied depending on how you build and set up your project.
Swift/ObjC requires doing whatever apple wants and dealing with their tooling.
Everything is different. If you want “one winner” the closest you get it is the system package manager (of which multiple exist) and pointing your project to its package cache. But not all system package managers handle multiple concurrent versions of the same package.
Maybe one day people will standardize against Nix which seems to be the closest?
In C++ (EVE, Vc, Highway, xsimd, stdlib), you can specify the ABI of a vector, which allows you to make platform specific optimizations in multifunctions. Or you can write vector code of a lowest-common-denominator width (like 16 bytes, rather than specifying the number of lanes), which runs the same on NEON and SSE2. Or you can write SIMD that is automatically natively optimized for just a single platform. These features are available on every notable C++ SIMD library, and they're basically indispensable for serious performance code.
https://odin-lang.org/docs/overview/#swizzle-operations
Matrix types are also built in:
https://odin-lang.org/docs/overview/#matrix-type
I’ve thought for a little while that Odin could be a secret weapon for game dev and similar pieces of software.
I also found the game dev libraries in Odin far easier to use then the ones in zig.
This certainty comes at a cost, either by negotiating with the Rust compiler or by putting up with a GC. But depending on your calculus, that cost might be worth paying.
Additionally, for efficient math code you often want vector / matrix types in AOSOA fasion: for example Vec3<Float8> to store an AVX lane for each X/Y/Z component. I want vector/matrix operations to work on SIMD lanes, not just for scalar types, and Zig currently can't support math operators on these kinds of types.
I ask if it's really simple, because the JVM space went in the same direction with Gradle (build script = program) and by the time it gets more sophisticated that can turn out to be pretty painful. In particular, IDEs struggle to get the information they need, scripts can become highly complex, and there are various other problems.
Alternatively, you can let the system package manager do all the hard work for you. That works great, as long as you only target one OS or put in the work to maintain compatibility with multiple OS releases.
My experience with languages without package managers is that large, maintained projects all invent their own package manager (remote git repos? shell scripts downloading files? a two-stage build process written in the language itself?) in combination with some sort of Makefile generating hell script that breaks for a while when a new OS release comes out.
This approach works if you're the entire system. SerenityOS can do this, because it's basically an entire operating system and userland all inside one repository. ChromeOS can probably do it as well, since the app portion isn't running OS-native code anyway. For everyone else, it's just making someone else do the work of the package manager manually.
Maybe I am just being too pedantic.
And then how do you maintain all of this bloat to a competitive level, so that users don't need to reach for faster/newer alternative packages anyway?
And how do you maintain portability and backward compatibility of a language with such a vast API surface?
In modern development there's just too much stuff to keep everyone happy with batteries included. Sooner or later users will need something that isn't built in, and that will cause pain and crappy workarounds if the language doesn't have first-class support for dependencies.
As for lifetimes, what's the issue? Do you have any reason to believe lifetimes are frustrating because they were badly designed, rather than the fact that they're making complexity explicit which was previously hidden?
Or if having the game on the system can be used by another malicious application as jumping point into root access, starting it as subprocess and injecting the exploit.
Example, Windows attacks via Notepad.
https://github.com/floooh/sokol-odin
It's a very enjoyable language!
So overall I don't view them in a very positive light. They're something I have to put up with.
No matter, it is what it is. Carry on. :)
"Any discussion on the foundations of computing runs into severe problems right at the start. The difficulty is that although we all use words such as ‘name’, ‘value’, ‘program’, ‘expression’ or ‘command’ which we think we understand, it often turns out on closer investigation that in point of fact we all mean different things by these words, so that communication is at best precarious."
Rather than debating the semantics of the colloquial usage of referential transparency, I'm more interested in the question: what can I tell at the call site of a function without knowing the definition of the function? In an impure language, I cannot tell whether the call has side effects without looking at the definition. This is true whether I am using a macro or simply a regular function call.
Now, even if my language of choice is impure, referential transparency of expressions is still a useful concept that can inform how I write my program. I can use naming, for example, to suggest whether a function call may have side effects even if the language compiler can't verify the property. Not perfect, but better than nothing. And if I'm really confused by a bug, I can always just assume that the name is misleading and the function may have unintentional side effects. In other words, I can use the concept of referential transparency to implement a metaprogramming system in my head.
[1] I can point at most Java code and prove how it fails the definition. It's not especially hand-wavey.
I agree with you, but this is subjective. Not having a package manager will probably turn off many from the language. But it’s OK for the Zig folks to make a call that a lot of people won’t agree with if it doesn’t fit their vision of the language.
Nim also has support for `ctypes` and compiles to C as its main target: yet though its interop is powerful it lacks in ergonomics, formerly you had to manually wrap every function you wished to use and this was only recently fixed by a macro-heavy external library.
I'm wondering what Zig does because IMO even if you have an excellent technical solution getting people to actually use it alongside C is hard, it has to be seamlessly seamless. Nim's C interop is rarely used outside of wrappers and it even more rarely is used to produce C libraries (though perhaps that's more a fault of incompatible type systems)
You could also use an existing language agnostic package manager like nix, guix, or conda to bootstrap your language package manager.
Lsp is something I don't know of a way to make that easy without overly constraining the design space.
In general this is true, but it is possible to write a hygienic macro engine that is referentially transparent. Many (likely most) macro engines are indeed unhygienic though. I am not convinced that comptime is a better approach than a well designed hygienic macro system but it is an interesting research area.
const c = @cImport({
@cDefine("SOME_MACRO", "1");
@cInclude("raylib.h");
});
Which translates the header files directly into Zig and allows you to call into them under whatever namespace you assigned them under. You even get completions (assuming you're using the language server)But, I manage to set it aside because it provides enough benefits, I mainly use Zig as a toolchain to crosscompile my libraries.. and write some helper externs
Hopefully they manage to improve and ease out the syntax by 1.0, I have hopes
But that was a mess. I still have nightmares from dealing with windows assembly cache issues.
Honestly it reminds me of some of the shit I've had to deal with with pip (what do you mean you can't resolve this dependency???)
Such beliefs are however compatible with the inveterate C programmer excuse that their nonsense programs should be correct if only the standards committee, compiler vendors, OS designers, and everybody else in the known universe were not conspiring to defeat their clear intent.
https://cljdoc.org/d/org.mentat/emmy/0.30.0/doc/data-types/m...
This is the complaint I was responding to. Here is that code in Clojure (a Lisp):
// What the GP claims is bad for doing math:
plus(a,b)
minus(a,b)
assign(a,b) // <= I have no idea what this does, or has to do with math.
// Let's actually use the original math operators, but with function notation:
+(a,b)
-(a,b)
// And here's the Clojure/Lisp syntax for the same:
(+ a b)
(- a b)
Lisp doesn't have "operators", so it doesn't have "operator overloading." What it does have is multi-dispatch, so yeah, the implementation of `+` can depend on the (dynamic) types of both `a` and `b`. That's a good thing, it means that the `+` and `-` tokens aren't hard-coded to whatever the language designer decided they should be in year 0, with whatever precedence and evaluation rules they picked at the time.The point I'm making is that you absolutely DO NOT need to have special-cased, infix math operators to "do math" in a reasonable, readable way. SICP is proof, and Emmy is a breeze to work with. And it turns out, there are a lot of advantages in NOT hard-coding your infix operators and precedence rules into the syntax of the language.
var t: ?i32 = null;
if (t) {
t +=1;
}
This for example doesn't work, you have to capture, many little things like that that stacks up and ends up creating a non-ergonomic syntaxNo for loop, so you have to do multilines things like, and now you meet a new : construct, this pseudo for loop now looks confusing to look at
var i: i32 = 0;
while (i < 42) : (i+=1) {
}
Then constant casting between integers, and also floats.. also no operator overload for the math types etc..Also not a fan of the pointer difference between [] * [:0], too much different nitpicking that makes iterating slow and painful, I understand the benefits, but the way it's done is not enjoyable, at least to me
That's just whats on the top of my head, it's been a while I haven't wrote Zig code so I may be remembering wrong
My emphasis. Undefined Behaviour is not an example of operations being defined.
This is 100% a real concern.
If I'm going to choose Zig, it's because it is SO much better on some axis that the community/adoption isn't an issue as I gain the benefits almost immediately. That means that C++ and Rust probably aren't in the scope of choice anyway.
I especially like the comment from elsewhere in thread where they are using it for scripting, of all things, because they can pop out code that builds and works on Windows and Linux. That's a good example--you gain the benefits immediately even if you later have to unwind that to something like Python.
However, better integrated package managers can work well. In case of Node.js and Cargo, the main argument against them is that it's too easy to add dependencies.
It's also the source of my major problem with zig. It doesn't have its own ABI [1].
So, if for example, you want to write a library in zig, to be used by others from zig, they must build your library with their project. That may not be an issue for smaller things; but for a large library I'd really like consumers to be able to pull in a binary with just a definition (header) file. Since zig uses the C ABI, that would currently mean translating everything to and from C at the binary interface, and losing all ziggyness in the process.
Are people realistically reviewing code formatting? As long as people aren't making egregious violations I generally don't care if someone leaves a brace on the same line or writes a one-line if statement. I tend to review the overall design and look for bugs and edge cases that might've been missed. If somebody told me they didn't like the way I formatted the code then they've got their own editor and are welcome to change it if they want to be petty.
I am not familiar with Emmy but I'm guessing that the usual work flow will involve an interactive shell with many calls to `render` to display expressions in infix notation so that you can better check if the expression you typed is actually what you meant to type.
The infix notation, although arbitrary and not as logically simple as other notations, is almost universal in the math-speaking world. Most mathematicians and engineers have have years of experience staring at infix expressions on blackboards, and disseminate new knowledge using this notation, and do new calculations in this notation.
In reading your reply, I think that maybe some tooling that could auto-insert corresponding infix-like comments above an AST-like syntax could be a solution for writing such code in Zig.
Everyone should take the time to setup their editor to format their code consistent with the project style, it doesn't take long at all.
People review code. As soon as that happens, formatting is part of the conversation. Either consciously or subconsciously
1. we never distributed debs for debian distributions. I have, however, been patiently collaborating with Debian maintainers with regards to the zig ITP: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=995670
2. we don't tell you to "just use a snap.". Please see https://github.com/ziglang/zig/#installation
3. we tag releases soon after llvm tags releases for the convenience of package maintainers. Distributions with LLVM 14 can package Zig 0.9.1; distributions with LLVM 15 can package Zig 0.10.1, etc.
<33
Zig's comptime is analogous to constinit, because it lets you compute a value at compile time and ensure that it is linker-initialized into the final binary image.
The point about CTRP is interesting, I'd have to think about that some more.
I think Zig falls into an "easy to learn, average/hard to master"
Thank you for sharing an insider's account. I naively assume that Zig is not your first language. Regarding "average/hard to master", can you think of any of languages where this is not true? Zero trolling, I promise. My point: No language is any less than average to master. Even VBA has some weird stuff in it that used to catch me off guard when I wrote Excel apps years ago. To be clear, I would classify VBA as similar, but I would classify Python as "easy to learn, very hard to master", and Perl as "average to learn, impossible to master"(!).If your language compiles fast enough, it's about the same experience as running a python script.
In pretty much all languages operators are just sugar for calling a method. There is no difference other than an easier to read syntax.
In rust for example, doing a + b is exactly the same as doing a.add(b).
In python it's exactly the same as doing a.__add__(b).
In C++, my understanding is that its sugar for a.operator+(b) or operator+(a, b).
I think there are some arguments against operator overloading but "spooky action at a distance" doesn't seem to be a very good one to me.
Because no has put in the effort to build it and get it adopted. You could also ask why we didn't have something like LSP earlier. In order for such a project to become widely adopted it would need to work well for at least a few popular languages, and work at least approximately as well as the native package management solutions for those languages. And it would need to be easy to use. You can kind of use some existing tools, such as maven, bazel, even npm, for other languages, but it usually isn't as nice of an experience.
I don’t know anything about ada/spark but it seems clear that something has gone terribly wrong for them to not have taken off like Rust has.
I agree that adding too many features can make a language too large and bloated. However, I disagree that this is always the case. For example, adding features that make it easier to code math is not necessarily a bad thing. In fact, it is a good thing, as it can make programming more accessible to a wider range of people.
Additionally, math is often used in fields that require high speed, such as computer graphics and game development, Computer vision, Robotics, Machine learning, Natural language processing (NLP), Mathematical modeling, all kinds of scientific computing (Computational physics, Computational chemistry, Computational biology...) As a result, low-level programming languages are often used to implement the core code in these fields. As you see, Math is essential for many fields.
Some overall differences between Odin and Zig and how I relate to them are:
## Exhaustive field setting
Zig requires you to set every field in a struct. Everything everywhere has to be initialized to something, even if it's `undefined` (which means it's the same thing as missing initialization in C, etc.).
Odin instead takes the position that everything is zero-initialized by default. It then also takes the position that everything has a zero value and that zero value is a valid value to have. The libraries think so, the users of the language think so and what you get when everything works like this is code that overwhelmingly talks exactly only about the things that need talking about, i.e. we only set the fields right now that matter.
Exhaustive field setting does remove uncertainty and I was very skeptical about just leaving things to zero-initialization, but overall I would say I prefer it. It really does work out most of the time and I've had plenty of bugs in Zig caused by setting something to `undefined` and just moving on, so it's not really as if that exhaustiveness check for fields was some 100% solution.
## An implicit context vs. explicit parameters
Zig is more or less known for using parameters to pass around allocators, and so on. It's not a new concept in most lower-level languages but it's one of the first languages to be known for baking this into the core community and libraries.
Odin does the same thing except it uses an implicit parameter in all Odin functions that is called `context`. When you want to change the allocator for a scope, you only need to set `context.allocator` (or `context.temp_allocator`) and every function you call in that scope will use that allocator. We can also write functions that take an optional parameter that defaults to the current allocator:
join :: proc(
a: []string,
sep: string,
allocator := context.allocator,
) -> (
res: string,
err: mem.Allocator_Error,
) {
}
This way we get the same behavior and flexibility of talking about allocators but we can also default to either the basic default or whatever the user currently has in scope. We *can* also be more explicit if we want. The ability to have this implicit again makes it so we only need to talk about the things that are special in the code.The context is also used for logger information, by the way, and you also have a `user_data` field that can be used to hold other stuff but I haven't really needed it for anything so far.
## Error information
Zig is known for its error unions, i.e. a set of error codes that can be inferred and returned from a function based on the functions it calls. These are nice and undoubtedly error handling in Zig is very neat because of it. However, there is a major downside: You can't actually attach a payload to these errors, so these are just tags that you propagate upwards. Not a huge deal but it is annoying; you'll have to have a parameter that you fill in when an error has occurred and you need more info:
// `expect_error` here is only filled in with something if we have an error
pub fn expect(self: *Self, expected_token: TokenTag, expect_error: *ExpectError) !Token {
}
Odin instead has a system that works much the same, we can return early by using what is effectively the same as `try` in Zig: `or_return`. This will check the last value in the return type of the called function to see if it's an error value and return that error value if it is.The error values that we talk about in Odin are just its normal values and can be tagged unions if we so choose. If we have the following Zig definitions for the `ExpectError` example:
pub const ExpectTokenError = struct {
expectation: TokenTag,
got: Token,
location: utilities.Location,
};
pub const ExpectOneOfError = struct {
expectations: []const TokenTag,
got: Token,
location: utilities.Location,
};
pub const ExpectError = union(enum) {
token: ExpectTokenError,
one_of: ExpectOneOfError,
};
We could represent them as follows and just use them as the error return value without holding a slot as a parameter that we will fill in: ExpectTokenError :: struct {
expectation: TokenTag,
got: Token,
location: Location,
}
ExpectOneOfError :: struct {
expectations: []TokenTag,
got: Token,
location: Location,
}
ExpectError :: union {
ExpectTokenError,
ExpectOneOfError,
}
expect_token :: proc(iterator: ^TokenIterator, expected_token: TokenTag) ->
(token: Token, error: ExpectError) {
}
They are normal tagged unions and in contrast to Haskell/Rust unions we don't have to bother with having different constructors for these just because a type is part of several different unions either, which is a big plus. We still get exhaustiveness checks, something akin to pattern matching with `switch tag in value { ... }`[0] and so on. Checking for a certain type in a union also is consistent across every union that contains that type, which is actually surprisingly impactful in terms of design, IMO.## Vectors are special
This isn't going to have a Zig equivalent because, well, Zig just doesn't.
Odin has certain names for the first, second, third, etc., positions in arrays. This is because it's specifically tailored to programmers that might deal with vectors. It also has vector and matrix arithmetic built in (yes, there is a matrix type).
array_long := [?]f32{1, 2, 3, 4, 5}
array_short := [?]f32{0, 1, 2}
fmt.printf("xyzw: %v\n", array_long.xyzw)
fmt.printf("rgba: %v\n", array_long.rgba)
fmt.printf("xyz * 2: %v\n", array_long.xyz * 2)
fmt.printf("zyx + 1: %v\n", array_long.zyx + 1)
fmt.printf("zyx + short_array: %v\n", array_long.zyx + array_short)
This gives the following output: > odin run swizzling
xyzw: [1.000, 2.000, 3.000, 4.000]
rgba: [1.000, 2.000, 3.000, 4.000]
xyz * 2: [2.000, 4.000, 6.000]
zyx + 1: [4.000, 3.000, 2.000]
zyx + short_array: [3.000, 3.000, 3.000]
It seems like a small thing but I would say that this has actually made a fair amount of my code easier to understand and write because I get to at least signal what things are.## Bad experiences
### Debug info
The debug information is not always complete. I found a hole in it last week. It's been patched now, which is nice, but it was basically a missing piece of debug info that would've made it so that you couldn't know whether you had a bug or the debug info was just not there. That makes it so you can't trust the debugger. I would say overall, though, that the debug info situation seems much improved in comparison to 2022 when apparently it was much less complete (DWARF info, that is, PDB info seems to have been much better overall, historically).
0 - https://odin-lang.org/docs/overview/#type-switch-statement
This is also why I struggle to answer the question "is it hard". Is it hard to build a web-app? No. But I've been building web-apps for 20 years. Will you find it hard (if you've never programmed before)? I don't know, if you want to build one this weekend, sure. If you have the patience to spend a few years learning first, then no.
Did you encounter memory bugs? Essentially what memory safety feature Odin offer? (From what I read here, Zig and Rust offer some features to eliminate entire classes of bugs which remove the nightmare of hours of debugging)
Anything you dislike or wish Odin has that other languages offer?
>that the debug info situation seems much improved in comparison to 2022 when apparently it was much less complete (DWARF info, that is, PDB info seems to have been much better overall, historically).
I believe Odin was developed on windows and other system come later, probably that why PDB is much better.
https://zigbyexample.github.io/
Their coverage is not very broad (in comparison to eg. Go by Example), but might be enough to get yourself started.
Odin is essentially in the same ballpark as Zig in terms of general safety features. Slices make dealing with blocks of things very easy in comparison to C, etc., and this helps a lot. Custom allocators make it easy to segment your memory usage up in scopes and that's basically how I deal with most things; you very rarely should be thinking about individual allocations in Odin, in my opinion.
> Anything you dislike or wish Odin has that other languages offer?
I would say that in general you have to be at least somewhat concerned with potential compiler bugs in certain languages and Odin would be one of them. That's not to say that I've stumbled on any interesting compiler bugs yet, but the fact that they very likely do exist because the compiler is a lot younger than `clang` or `gcc` makes it something that just exists in the background. Multiply that by some variable amount when something is more experimental or less tried and true. The obvious example there is the aforementioned debug info where on Linux this has been tried less so it is more likely to be worse, and so on.
In an ideal (fantasy) world I'd love something like type classes (from Haskell) in Odin; constraints on generic types that allow you to write code that can only do exactly the things expressed by those constraints. This gives you the capability to write code that is exactly as generic as it can logically be but no more and no less. Traits in Rust are the same thing. With that said, I don't believe that neither Haskell nor Rust implements them in a way that doesn't ruin compile time. Specialization of type classes is an optimization pass that basically has to exist and even just the fact that you have to search all your compiled code for an instance of a type class is probably prohibitively costly. It's very nice for expression but unless Odin could add them in a way that was better than Haskell/Rust I don't think it's worth having.
I would like to see how uniform function call syntax would work in Odin but this is actually addressed in the FAQ here: https://odin-lang.org/docs/faq/#why-does-odin-not-have-unifo...
UFCS works really well in D but D also has ad-hoc function overloading whereas Odin has proc groups. I think UFCS only really works with exceptions as well, so I think it can become awkward really fast with the amount of places you want to return multiple values where your last one represents a possible error.
EDIT: Oh, I think I see how it's like `constinit`. If you have a non-`comptime` variable that is initialized by a `comptime` function, then it's a non-constant constant-initialized variable.
We've been spoiled by good LSPs, perhaps. But again, it's not just "parse this file please".
Other than that, I was and I am, mostly a frontend developer with ECMAScript and TypeScript experience. I think Zig is very close to both, because Zig has anytype, so you can do duck-typing like you do in JS, and it has programmable type system, just like TS. Not to mention that reflection is basically your daily bread in JS/TS.
TLDR: If you have some systems programming experience and you've done a bit of TS, I'd definitely recommend you to give Zig a try. One weekend should be enough. I did that literally single-handed, as I was recovering after wrist surgery.
To attempt to answer your question, you might be surprised to learn just how much Ada/spark exists in the world if you go looking. Most of it is not open source. I'm not sure they haven't "taken off like Rust has" because Rust is still very niche, kinda like Ada is
One-shot programs also don't always need it as they're often fine with a less sophisticated scheme of arena allocation or reusing memory by resetting temporary buffers.
You also have the option to classify pointers if you absolutely must pass them with similar techniques as https://dinfuehr.github.io/blog/a-first-look-into-zgc.
The documentation, especially for the build system, was lacking. But the interop itself is pretty smooth. Only rough point is that I can't define a struct type in Zig and use it in C; you HAVE to define your structs in C if you plan to pass them between the languages (as far as I know anyways).
Zig has documentation on making your Zig structs match the layout of C structs[1] so I assume the intended use case is not as I have done (use the C type in Zig) but instead of define a matching Zig type and cast your C structs into Zig structs, or vice versa, at the boundary between languages.
Structs aside, for functions you just stick "export" on the Zig functions to make them available at link time (presumably) and use "callconv(.C)" so C can call them cleanly. Very easy.
> Exhaustive field setting does remove uncertainty and I was very skeptical about just leaving things to zero-initialization, but overall I would say I prefer it. It really does work out most of the time...
Vlang does this as well (also influenced by Wirth/Pascal/Oberon/Go). Overall, this is an advantage and greater convenience for users.