this is a really, really good article with a lot of nuance and a deep understanding of the tradeoffs in syntax design. unfortunately, it is evoking a lot of knee-jerk reactions from the title and emotional responses to surface level syntax aesthetics.
the thing that stands out to me about Zig's syntax that makes it "lovely" (and I think matklad is getting at here), is there is both minimalism and consistency to the design, while ruthlessly prioritizing readability. and it's not the kind of surface level "aesthetically beautiful" readability that tickles the mind of an abstract thinker; it is brutalist in a way that leaves no room for surprise in an industrial application. it's really, really hard to balance syntax design like this, and Zig has done a lovely and respectable job at doing so.
scuff3d · 2d ago
My only complaint about the article is that it doesn't mention error handling. Lol
Zigs use of try/catch is incredible, and by far my favorite error handling of any language. I feel like it would have fit into this article.
Twey · 2d ago
> it's not the kind of surface level "aesthetically beautiful" readability that tickles the mind of an abstract thinker
Rather, the sort of beauty it's going for here is exactly the type of beauty that requires a bit of abstraction to appreciate: it's not that the concrete syntax is visually beautiful per se so much as that it's elegantly exposing the abstract syntax, which is inherently more regular and unambiguous than the concrete syntax. It's the same reason S-exprs won over M-exprs: consistently good often wins over special-case great because the latter imposes the mental burden of trying to fit into the special case, while the former allows you to forget that the problem ever existed. To see a language do the opposite of this, look at C++: the syntax has been designed with many, many special cases that make specific constructs nicer to write, but the cost of that is that now you have to remember all of them (and account for all of them, if templating — hence the ‘new’ uniform initialization syntax[1]).
This trade-off happens all the time in language design: you're looking for language that makes all the special cases nice _as a consequence of_ the general case, because _just_ being simple and consistent leads you to the Turing tarpit: you simplify the language by pushing all the complexity onto the programmer.
n42 · 2d ago
I considered making the case for the parallels to Lisp, but it's not an easy case to make. Zig is profoundly not a Lisp. However, in my opinion it embodies a lot of the spirit of it. A singular syntax for programming and metaprogramming, built around an internally consistent mental model.
I don't really know how else to put it, but it's vaguely like a C derived spiritual cousin of Lisp with structs instead of lists.
Twey · 2d ago
I think because of the forces I talked about above we experience a repeating progression step in programming languages:
- we have a language with a particular philosophy of development
- we discover that some concept A is awkward to express in the language
- we add a special case to the language to make it nicer
- someone eventually invents a new base language that natively handles concept A nicely as part of its general model
Lisp in some sense skipped a couple of those progressions: it had a very regular language that didn't necessarily have a story for things that people at the time cared about (like static memory management, in the guise of latency). But it's still a paragon of consistency in a usable high-level language.
I agree that it's of course not correct to say that Zig is a descendent or modern equivalent of Lisp. It's more that the virtue that Lisp embodies over all else is a universal goal of language design, just one that has to be traded off against other things, and Zig has managed to do pretty well at it.
bsder · 2d ago
> I don't really know how else to put it, but it's vaguely like a C derived spiritual cousin of Lisp with structs instead of lists.
Zig comptime operates a lot like very old school Lisp FEXPRS before the Lisp intelligentsia booted them out because FEXPRS were theoretically messy and hard to compile.
fouric · 1d ago
As someone who loves Lisps, I still have to disagree on the value of the s-expression syntax. I think that sexps are very beautiful, easy to parse, and easy to remember, but I think that overall they're less useful than Algol-like syntaxes (of which I consider most modern languages, including C++, to be in the family of), for one reason:
Visually-heterogeneous syntaxes, for all of their flaws, are easier to read because it's easier for the human brain to pattern-match on distinct features than indistinct ones.
pyinstallwoes · 2d ago
So urbit’s nock vs forth?
pyinstallwoes · 2d ago
Have any examples that stand out to you to share ?
bscphil · 2d ago
> Like Rust, Zig uses 'name' (':' Type)? syntax for ascribing types, which is better than Type 'name'
I'm definitely an outlier on this given the direction all syntactically C-like new languages have taken, but I have the opposite preference. I find that the most common reason I go back to check a variable declaration is to determine the type of the variable, and the harder it is to visually find that, the more annoyed I'm going to be. In particular, with statically typed languages, my mental model tends to be "this is an int" rather than "this is a variable that happens to have the type 'int'".
In Rust, in particular, this leads to some awkward syntactic verbosity, because mutable variables are declared with `let mut`, meaning that `let` is used in every declaration. In C or C++ the type would take the place of that unnecessary `let`. And even C (as of C23) will do type inference with the `auto` keyword. My tendency is to use optional type inference in places where needing to know the type isn't important to understand the code, and to specify the type when it would serve as helpful commentary when reading it back.
SkiFire13 · 2d ago
> In C or C++ the type would take the place of that unnecessary `let`
In my opinion the `let` is not so unnecessary. It clearly marks a statement that declares a variable, as opposed to other kind of statements, for example a function call.
This is also why C++ need the "most vexing parse" ambiguity resolution.
reactordev · 2d ago
I’m in the same boat. It’s faster mentally to grok the type of something when it comes first. The name of the thing is less important (but still important!) than the type of the thing and so I prefer types to come before names.
From a parser perspective, it’s easier to go name first so you can add it to the AST and pass it off to the type determiner to finish the declaration. So I get it. In typescript I believe it’s this way so parsers can just drop types all together to make it compatible with JavaScript (it’s still trivial to strip typing, though why would you?) without transpiling.
In go, well, you have even more crazier conventions. Uppercase vs lowercase public vs private, no inheritance, a gc that shuns away performance minded devs.
In the end, I just want a working std library that’s easy to use so I can build applications. I don’t care for:
type Add<A extends number, B extends number> = [
…Array<A>,
…Array<B>,
][“length”]
This is the kind of abuse of the type system that drives me bonkers. You don’t have to be clever, just export a function. I don’t need a type to represent every state, I need intent.
AnimalMuppet · 2d ago
I, too, go back up the code to find the type of a variable. But I see the opposite problem: if the type is first, then it becomes harder to find the line that declares the variable I'm interested in, because the variable name isn't first. It's after the type, and the type could be "int" or it could be "struct Frobnosticator". That is, the variable name is after a variable-length type, so I have to keep bouncing left to right to find the start of the variable name.
Quekid5 · 2d ago
> I find that the most common reason I go back to check a variable declaration is to determine the type of the variable,
Hover the mouse cursor over it. Any reasonable editor will show the type.
> In Rust, in particular, this leads to some awkward syntactic verbosity, because mutable variables are declared with `let mut`, meaning that `let` is used in every declaration.
Rust is very verbose for strange implementation reasons... namely to avoid parse ambiguities.
> In C or C++ the type would take the place of that unnecessary `let`.
OTOH, that means you can't reliably grep for declarations of a variable/function called "foo". Also consider why some people like using
auto foo(int blah) -> bool
style. This was introduced because of template nonsense (how to declare a return type before the type parameters were known), but it makes a lot of sense and makes code more greppable. Generic type parameters make putting the return type at the front very weird -- reading order wise.
Anyhoo...
bscphil · 7h ago
>> I find that the most common reason I go back to check a variable declaration is to determine the type of the variable,
> Hover the mouse cursor over it. Any reasonable editor will show the type.
That applies to most of us, obviously, but in this context we're talking about Zig. Zig's lead developer, Andrew Kelley, programs in Vim with no autocomplete or mouse support.
Even though I sometimes use editors with these features, I find it frustrating when languages seem to be designed in such a way that presumes their availability. I found Rust particularly bad about this, for example.
MoltenMan · 2d ago
I much prefer Pascal typing because it
1. Allows type inference without a hacky 'auto' workaround like c++ and
2. Is less ambiguous parsing wise. I.e. when you read 'MyClass x', MyClass could be a variable (in which case this is an error) or a type; it's impossible to know without context!
erk__ · 2d ago
Maybe it just have to do with what you are used to, it was one of the things that made me like Rust coming from F# it had the same `name : type` and `let mutable name` that I knew from there.
z_open · 2d ago
> Raw or multiline strings are spelled like this:
const still_raw =
\\const raw =
\\ \\Roses are red
\\ \\ Violets are blue,
\\ \\Sugar is sweet
\\ \\ And so are you.
\\ \\
\\;
\\
;
This syntax seems fairly insane to me.
IshKebab · 2d ago
Maybe if you've never tried formatting a traditional multiline string (e.g. in Python, C++ or Rust) before.
If it isn't obvious, the problem is that you can't indent them properly because the indentation becomes part of the string itself.
Some languages have magical "removed the indent" modes for strings (e.g. YAML) but they generally suck and just add confusion. This syntax is quite clear (at least with respect to indentation; not sure about the trailing newline - where does the string end exactly?).
qzzi · 2d ago
C and Python automatically concatenate string literals, and Rust has the concat! macro. There's no problem just writing it in a way that works correctly with any indentation. No need for weird-strings.
" one\n"
" two\n"
" three\n"
drjeats · 1d ago
Personally, I'd rather prefix with `\\` than have to postfix with `\n`. The `\\` is automatically prepended when I enter a newline in my editor after I start a multiline string, much like editors have done for C-style multiline comments for years.
Snippet from my shader compiler tests (the `\` vs `/` in the paths in params and output is intentional, when compiled it will generate escape errors so I'm prodded to make everything `/`):
I may be missing something but come Go has a simple:
`A
simple
formatted
string
`
?
rybosome · 2d ago
Yours is rendered as:
A\n\tsimple\n\t\tformatted\n\t\t\tstring\n\t
If you wanted it without the additional indentation, you’d need to use a function to strip that out. Typescript has dedent which goes in front of the template string, for example. I guess in Zig that’s not necessary which is nice.
konart · 1d ago
Okay I get it now. Formatted multiline string in code without an actual formatting and no string concat.
xigoi · 2d ago
The problem is that usually you have something like
Significant whitespace is not difficult to add to a language and, for me, is vastly superior than what zig does both for strings and the unnecessary semicolon that zig imposes by _not_ using significant whitespace.
I would so much rather read and write:
let x = """
a
multiline string
example
"""
than
let x =
//a
//multiline string
//example
;
In this particular example, zig doesn't look that bad, but for longer strings, I find adding the // prefix onerous and makes moving strings around different contexts needlessly painful. Yes, I can automatically add them with vim commands, but I would just rather not have them at all. The trailing """ is also unnecessary in this case, but it is nice to have clear bookends. Zig by contrast lacks an opening bracket but requires a closing bracket, but the bracket it uses `;` is ambiguous in the language. If all I can see is the last line, I cannot tell that a string precedes it, whereas in my example, you can.
Here is a simple way to implement the former case: require tabs for indentation. Parse with recursive descent where the signature is
Multiline string parsing becomes a matter of bumping the indent parameter. Whenever the parser encounters a newline character, it checks the indentation and either skips it, or if is less than the current indentation requires a closing """ on the next line at a reduced indentation of one line.
This can be implemented in under 200 lines of pure lua with no standard library functions except string.byte and string.sub.
It is common to hear complaints about languages that have syntactically significant whitespace. I think a lot of the complaints are fair when the language does not have strict formatting rules: python and scala come to mind as examples that do badly with this. With scala, practically everyone ends up using scalafmt which slows down their build considerably because the language is way too permissive in what it allows. Yaml is another great example of significant whitespace done poorly because it is too permissive. When done strictly, I find that a language with significant whitespace will always be more compact and thus, in my opinion, more readable than one that does not use it.
I would never use zig directly because I do not like its syntax even if many people do. If I was mandated to use it, I would spend an afternoon writing a transpiler that would probably be 2-10x faster than the zig compiler for the same program so the overhead of avoiding their decisions I disagree with are negligible.
Of course from this perspective, zig offers me no value. There is nothing I can do with zig that I can't do with c so I'd prefer it as a target language. Most code does not need to be optimized, but for the small amount that does, transpiling to c gives me access to almost everything I need in llvm. If there is something I can't get from c out of llvm (which seems highly unlikely), I can transpile to llvm instead.
winwang · 2d ago
Does `scalafmt` really slow down builds "considerably"? I find that difficult to believe, relative to compile time.
z_open · 2d ago
Even if we ignore solutions other languages have come up with, it's even worse that they landed on // for the syntax given that it's apparently used the same way for real comments.
WCSTombs · 2d ago
But those are two different slashes? \\ for strings and // for comments?
IshKebab · 2d ago
Yeah I agree \\ is not the best choice visually (and because it looks quite similar to //
I would have probably gone with ` or something.
n42 · 2d ago
> it's even worse that they landed on // for the syntax
.. it is using \\
hinkley · 2d ago
I worked with browsers since before most people knew what a browser was and it will never cease to amaze me how often people confuse slash and backslash, / and \
It’s some sort of mental glitch that a number of people fall into and I have absolutely no idea why.
Mawr · 2d ago
I doubt those very people would confuse the two when presented with both next to each other: / \ / \. The issue is, they're not characters used day-to-day so few people have made the association that the slash is the one going this way / and not the one going the other way \. They may not even be aware that both exist, and just pick the first slash-like symbol they see on their keyboards without looking further.
Twey · 2d ago
I wonder if it's dyslexia-adjacent. Dyslexic people famously have particular difficulty distinguishing rotated and reflected letterforms.
hinkley · 2d ago
Could be. The frequency is such that it could be dyslexics. It's not all the time, but it's a steady rate of incidence.
quesera · 2d ago
I think in the 90's it was just people repeating a pattern they learned from Windows/DOS.
It used to grate on my nerves to hear people say, e.g. "H T T P colon backslash backslash yahoo dot com".
But I think they always typed forward slash, like they knew the correct slash to use based on the context, but somehow always spoke it in DOSish.
Blackarea · 2d ago
We can just use a crate for that and don't have to have this horrible comment like style that brings its own category of problems. https://docs.rs/indoc/latest/indoc/
Twey · 2d ago
And what if you do want to include two spaces at the beginning of the block (but not any of the rest of the indentation)?
Choice of specific line-start marker aside, I think this is the best solution to the indented-string problem I've seen so far.
gf000 · 2d ago
I think Java's solution is much cleaner.
IshKebab · 2d ago
For those of us that haven't used Java for a decade...
> In text blocks, the leftmost non-whitespace character on any of the lines or the leftmost closing delimiter defines where meaningful white space begins.
It's not a bad option but it does mean you can't have text where every line is indented. This isn't uncommon - e.g. think about code generation of a function body.
gf000 · 1d ago
Why couldn't you have it?
You just put the ending """ where you want it relative to the content.
IshKebab · 1d ago
Ah I see - didn't notice it includes the trailing """. Tbh I still prefer Zig's solution. It's more obvious. (Though they should have picked a less intrusive prefix, I'd have gone with a backtick.)
Twey · 2d ago
Right, this is a pretty common syntax, but doesn't address the same problem as Zig's syntax.
I've only seen two that do: the Zig approach, and a postprocessing ‘dedent’ step.
n42 · 2d ago
Zig does not really try to appeal to window shoppers. this is one of those controversial decisions that, once you become comfortable with the language by using it, you learn to appreciate.
spoken as someone who found the syntax offensive when I first learned it.
ivanjermakov · 2d ago
It is not the insane syntax, but quite insane problem to solve.
Usually, representing multiline strings within another multiline string requires lots of non-trivial escaping. This is what this example is about: no escaping and no indent nursery needed in Zig.
whitehexagon · 2d ago
I think Kotlin solves it quite nicely with the trimIndent. I seem to recall Golang was my fav, and Java my least, although I think Java also finally added support for a clean text block.
Makes cut 'n' paste embedded shader code, assembly, javascript so much easier to add, and more readable imo. For something like a regular expressions I really liked Golang's back tick 'raw string' syntax.
In Zig I find myself doing an @embedFile to avoid the '\\' pollution.
rybosome · 2d ago
Visually I dislike the \\, but I see this solves the problem of multiline literals and indentation in a handy, unambiguous way. I’m not actually aware of any other language which solves this problem without a function.
throw10920 · 2d ago
It seems very reasonable and comes with several technical and cognitive advantages. I think you're just having a knee-jerk emotional reaction because it's different than what you're used to, not because it's actually bad.
conaclos · 2d ago
I like the idea of repeating the delimiter on every line. However `//` looks like a comment to me. I could simply choose double quote:
const still_raw =
"const raw =
" "Roses are red
" " Violets are blue,
" "Sugar is sweet
" " And so are you.
" "
";
"
;
This cannot be confused with a string literal because a string literal cannot contain newline feeds.
flexagoon · 2d ago
What if you have something like
const raw =
"He said "Hello"
"to me
;
Wouldn't that be a mess to parse? How would you know that "He said " is not a string literal and that you have to continue parsing it as a multiline string? How would you distinguish an unclosed string literal from a multiline string?
hardwaregeek · 2d ago
My immediate thought was hmm, that's weird but pretty nice. The indentation problem indeed sucks and with a halfway decent syntax highlighter you can probably de-emphasize the `//` and make it less visually cluttered.
conorbergin · 2d ago
I think everyone has this reaction until they start using it, then it makes perfect sense, especially when using editors that have multiple cursors and can operate on selections.
seabombs · 2d ago
I think the syntax highlighting for this could make it more readable. Make the leading `\\` a different color to the string content.
zem · 1d ago
that was my favourite bit in the entire post - the one place where zig has unambiguously one-upped other languages. the problems it is solving are:
1. from the user's point of view, you can now have multiline string literals that are properly indented based on their surrounding source code, without the leading spaces being treated as part of the string
2. from an implementation point of view having them parsed as individual lines is very elegant, it makes newline characters in the code unambiguous and context independent. they always break up tokens in the code, regardless of whether they are in a string literal or not.
steveklabnik · 2d ago
I had the exact opposite reaction.
klas_segeljakt · 2d ago
When I first read it I thought it was line comments.
watersb · 2d ago
Upvoting because similar comments here suggest that you are not alone.
People are having trouble distinguishing between '//' and '\\'.
fcoury · 2d ago
I really like zig but that syntax is indeed insane.
phplovesong · 2d ago
I find Zig syntax noicy. I dont like the @TypeOf (at symbol) and pals, and the weird .{.x} syntax feels off.
Zig has some nice things going on but somehow code is really hard to read, admitting its a skill issue as im not that versed in zig.
dsego · 2d ago
Zig is noisy and and the syntax is really not elegant. One reason I like odin's syntax, it's minimal and so well thought out.
ngrilly · 1d ago
Yes, Zig’s syntax is a bit noisier, but it enables things such as using if/for/while/switch in expressions, or using anonymous struct literals to emulate named and default parameters in functions.
flohofwoe · 2d ago
The dot is just a placeholder for an inferred type, and IMHO that makes a lot of sense. E.g. you can either write this:
const p = Point{ .x = 123, .y = 234 };
...or this:
const p: Point = .{ .x = 123, .y = 234 };
When calling a function which expects a Point you can omit the verbose type:
takePoint(.{ .x = 123, .y = 234 });
In Rust I need to explicitly write the type:
takePoint(Point{ x: 123, y: 234);
...and in nested struct initializations the inferred form is very handy, e.g. Rust requires you to write this (not sure if I got the syntax right):
...but the compiler already knows that Rect consists of two nested Points, so what's the point of requiring the user to type that out? So in Zig it's just:
Requiring the explicit type on everything can get noisy really fast in Rust.
Of course the question is whether the leading dot in '.{' could be omitted, and personally I would be in favour of that. Apparently it simplifies the parser, but such implementation details should get in the way of convenience IMHO.
And then there's `.x = 123` vs `x: 123`. The Zig form is copied from C99, the Rust form from Javascript. Since I write both a lot of C99 and Typescript I don't either form (and both Zig and Rust are not even close to the flexibility and convenience of the C99 designated initialization syntax unfortunately).
Edit: fixed the Rust struct init syntax.
do_not_redeem · 2d ago
Zig is planning to get rid of explicit `T{}` syntax, in favor of only supporting inferred types.
How is it hostile? The lsp can do type inference just the same as the compiler.
dminik · 1d ago
Yes. And by "do type inference just the same as the compiler", that includes having to do comptime execution. Plus with no interfaces/traits/concepts and comptime being mostly any-typed makes it difficult to have helpful intellisense.
flohofwoe · 2d ago
Ah, I wasn't aware of that proposal. But yeah in that case I would also heavily prefer to "drop the dot" :)
IMHO Odin got it exactly right. For a variable with explicit type:
a_variable : type = val;
...or for inferred type:
a_variable := val;
...and the same for constants:
a_const : type : val;
a_const :: val;
...but I think that doesn't fit into Zig's parser design philosophy (e.g. requiring some sort of keyword upfront so that the parser knows the context it's in right from the start instead of delaying that decision to a later time).
Defletter · 1d ago
That's... honestly really disappointing. I use explicit `T{}` because otherwise becomes too unreadable, too Assembly-like: I like knowing what types I'm using. It also provides a convenient thing to click on to inspect the type. I genuinely do not understand this headlong pursuit of conciseness to the detriment of readability.
hinkley · 2d ago
Thanks for the explanation, but I don’t think you’ve sold me on .x
Think I’d rather do the Point{} syntax.
No comments yet
tialaramex · 2d ago
Because we've said x is a constant we're obliged to specify its type. For variables we're allowed to use inference and in most cases the type can be correctly inferred, but for constants or function signatures inference is deliberately prohibited.
const x: Rect = ....
[Note that in Zig what you've written isn't a constant, Zig takes the same attitude as C and C++ of using const to indicate an immutable rather than a constant]
flohofwoe · 1d ago
> Zig takes the same attitude as C and C++ of using const to indicate an immutable rather than a constant
I think it's a bit more complicated than that: AFAIK Zig consts without explicit type may be comptime_int or comptime_float, and those don't exist at runtime. Only consts with an explicit type annotation are 'runtime consts'.
...still, I think Rust should allow to infer the type at least inside struct initialization, it would make designated init code like this a lot less noisy (Zig would suffer from the same problem if it hadn't the .{} syntax):
Surely Rust can infer the type in your example? It just doesn't provide Zig's syntax to use the inferred type to manually initialize. If you wrote some_fn() here where some_fn's return type was genericised, Rust would ask for the appropriately typed some_fn not say it doesn't know the type.
...Zig is still only halfway there compared to C99 (e.g. Zig doesn't allow designator chaining and is much less flexible for initializing nested arrays - in those areas it's closer to Rust than C).
tialaramex · 1d ago
Right but your call to Default::default() gives the game away that we do have inference. Default::default() is generic, if we didn't have inference we'd need to tell it which of the enormous number of implementations of that trait it should call.
Would you take syntax like clear_value: _ { r: 0.25, g: 0.5, g: 0.75, a: 1.0 } ?? Then we're saying that we know we need to pick a type here but the type can be inferred where our underscore was, just like when we
let words: Vec<_> = "A sentence broken by spaces".split_whitespace().collect();
kcartlidge · 2d ago
I much prefer C# 11's raw string literals. It takes the indentation of the first line and assumes the subsequent ones have the same indentation.
And it even allows for using embedded curly braces as real characters:
string json = $$"""
<h1>{{title}}</h1>
<article>
Welcome to {{sitename}}, which uses the <code>{sitename}</code> syntax.
</article>
""";
The $ (meaning to interpolate curly braces) appears twice, which switches interpolation to two curly braces, leaving the single ones untouched.
Metasyntactic · 2d ago
Just a minor correction (as I'm the author of c#'s raw string literal feature).
The indentation of the final ` """` line is what is removed from all other lines. Not the indentation of the first line. This allows the first line to be indented as well.
Cheers, and I'm glad you like it. I thought we did a really good job with that feature :-)
panzerboiler · 1d ago
Did you draw inspiration from Swift's multiline string literal, or was it the other way around? The syntax looks very similar, if not identical.
kcartlidge · 1d ago
Thanks for the correction. I never read the spec, just started using it. And as I tend to balance my first and last line indentation I never realised.
gf000 · 2d ago
Really not trying to go into any of the "holy wars" here, but could you please compare C#'s feature to Java's multi-line strings? I'm only familiar with the latter, and I would like to know if they are similar in concept or not.
winwang · 2d ago
That's a fantastic design idea, and it seems to require all the other lines to have the same indentation "prefix".
Haven't used much C#, but I love Scala's `.stripPrefix` and `StringContext`.
nromiun · 2d ago
I like Zig as well, but I won't call its syntax lovely. Go shows you can do pretty well without ; for line breaks, without : for variable types etc.
But sure, if you only compare it with Rust, it is a big improvement.
nine_k · 2d ago
I personally find Go's bare syntax harder to parse when reading, and I spend more time reading code than typing it (even while writing).
An excessively terse syntax becomes very unforgiving, when a typo is not noticed by the compiler / language server, but results in another syntactically correct but unexpected program, or registers as a cryptic error much farther downstream. Cases in point: CoffeeScript, J.
nromiun · 2d ago
That is why syntax debates are so difficult. There is no objectively best syntax. So we are all stuck with subjective experience. For me I find Python (non-typed) and Golang syntax easiest to read.
Too many symbols like ., :, @, ; etc just mess with my brain.
aatd86 · 2d ago
Yes it's sigils that are the culprits more often than not.
They are often semantically irrelevant and just make things easier to parse for the machines.
Happy Go doesn't indulge too much in them.
True. But my point is that adding stuff doesn't necessarily make the syntax better either. Otherwise we would all be using Perl by now. The sweet spot is somewhere in the middle.
ruuda · 2d ago
> as name of the type, I think I like void more than ()
It's the wrong name though. In type theory, (), the type with one member, is traditionally called "Unit", while "Void" is the uninhabited type. Void is the return type of e.g. abort.
cornstalks · 2d ago
It fulfills the same role as C and C++'s void type. I don't think most systems programmers care about type theorist bikeshedding about purity with formal theory.
A ton of people coming to Zig are going to be coming from C and C++. void is fine.
ivanjermakov · 2d ago
If I understood `abort` semantics correctly, it has a type of `never` or Rust's `!`. Which has a meaning "unobtainable value because control flow went somwhere else". `void` is closer to `unit` or `()` because it's the type with no allowed values.
Cool trick: some languages (e.g. TypeScript) allow `void` generics making parameters of that type optional.
steveklabnik · 2d ago
Unit has exactly one allowed value.
ivanjermakov · 2d ago
Right, but there is not much semantic difference in having one or having zero allowed values.
NobodyNada · 2d ago
There's a huge semantic difference: a type with zero allowed values can never be constructed.
This means that a function that returns an uninhabited type is statically known to not return -- since there's no way it could construct an uninhabited value for a return expression. It also means you can coerce a value of uninhabited type into any other type, because any code which recieves a value of an uninhabited type must be unreachable.
For instance, in Rust you can write the following:
Because panic! aborts the program and doesn't return, its return type is uninhabited. Thus, a panic! can be used in a context where a string is expected.
ivanjermakov · 1d ago
We were talking about `void`, not `!`. It's clear that "never" type has a special language support. Difference between `void` and `()` is much less subtle.
steveklabnik · 2d ago
One vs zero is an incredibly fundamental difference.
netbsdusers · 1d ago
The void type has considerable heritage, dating back all the way to ALGOL 68, and is traditionally defined as having one member:
> The mode VOID has a single value denoted by EMPTY.
bmacho · 2d ago
[deleted]
steveklabnik · 2d ago
It doesn’t work because you’re trying to pass unit to sayhi, which doesn’t take any arguments.
No comments yet
ww520 · 2d ago
Zig is great. I have fun writing in it. But there’re a few things bug me.
- Difficult to return a value from a block. Rust treats the value of the last expression of a block as the return value of the block. Have to jump through hoops in Zig to do it with label.
- Unable to chain optional checks, e.g. a?.b?.c. Support for monadic types would be great so general chaining operations are supported.
- Lack of lambda support. Function blocks are already supported in a number of places, i.e. the for-loop block and the catch block.
pton_xd · 2d ago
"Zig doesn’t have lambdas"
This surprises me (as a C++ guy). I use lambdas everywhere. What's the standard way of say defining a comparator when sorting an array in Zig?
tapirl · 2d ago
Normal function declarations.
This is indeed a point which makes Zig inflexible.
lenkite · 2d ago
By adopting a syntax like
fn add(x: i32, i32) i32
they have said perma-goodbye to lambdas. They should have at-least considered
fn add(x: i32, i32): i32
bitwizeshift · 2d ago
Why “perma goodbye”?
Go has a similar function declaration, and it supports anonymous functions/lambdas.
E.g. in go, an anonymous func like this could be defined as
foo := func(x int, _ int) int {
…
}
So I’d imagine in Zig it should be feasible to do something like
var foo = fn(x: i32, i32) i32 {
…
}
unless I’m missing something?
lenkite · 2d ago
Anonymous functions aren't the same as lambda functions. People in the Go community keep asking for lambda functions and never get them. There should be no need for func/fn and explicit return. Because the arrow would break stuff is one of the reasons.
res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
Reduce(func(a, b int) int { return a + b })
vs
res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)
hinkley · 2d ago
They could still fix it with arrow functions, but it’s always gonna look weird.
Some other people have tried to explain how they prefer types before variable declarations, and they’ve done a decent job of it, but it’s the function return type being buried that bothers me the most. Since I read method signatures far more often than method bodies.
fn i32 add(…) is always going to scan better to me.
lenkite · 2d ago
OK, but with generics return type first tends to becomes a monster.
hinkley · 1d ago
I used to be very enthusiastic about generic types. Now, well what else would you do? I don’t mean that as a rhetorical question. If someone came up with another way to represent functions that can take multiple types and knows what it will return, I’d be all over them.
Elixir is trying something, I don’t know yet whether it will be better. But their solution is based on a decision about how to do overloading that I suspect makes for maintenance problems later. So it’s gonna have to be good to offset the consequence.
jmull · 2d ago
You can declare an anonymous struct that has a function and reference that function inline (if you want).
There's a little more syntax than a dedicated language feature, but not a lot more.
What's "missing" in zig that lambda implementations normally have is capturing. In zig that's typically accomplished with a context parameter, again typically a struct.
veber-alex · 2d ago
So basically, Zig doesn't have lambdas, but because you still need lambdas, you need to reinvent the wheel each time you need it?
Why don't they just add lambdas?
jmull · 2d ago
> So basically...
Well, not really.
Consider lambdas in C++ (that was the perspective of the post I replied to). Before lambdas, you used functors to do the same thing. However, the syntax was slightly cumbersome and C++ has the design philosophy to add specialized features to optimize specialized cases, so they added lambdas, essentially as syntactic sugar over functors.
In zig the syntax to use an anonymous struct like a functor and/or lambda is pretty simple and the language has the philosophy to keep the language small.
Thus, no need for lambdas. There's no re-inventing anything, just using the language as it designed to be used.
flipgimble · 2d ago
Because to use lambdas you're asking the language to make implicit heap allocations for captured variables. Zig has a policy that all allocation and control flow are explicit and visible in the code, which you call re-inventing the wheel.
Lambdas are great for convenience and productivity. Eventually they can lead to memory cycles and leaks. The side-effect is that software starts to consume gigabytes of memory and many seconds for a task that should take a tiny fraction of that. Then developers either call someone who understands memory management and profiling, or their competition writes a better version of that software that is unimaginably faster. ex. https://filepilot.tech/
do_not_redeem · 2d ago
Nothing about lambdas requires heap allocation. See also: C++, Rust
porridgeraisin · 2d ago
If the lambda captures some value, and also outlives the current scope, then that captured value has to necessarily be heap allocated.
noselasd · 2d ago
No, (in C++) the lambda can capture the the variable by value, and the lambda itself can be passed around by value. If you capture a variable by reference or pointer that your lambda outlives, your code got a serious bug.
gf000 · 2d ago
And in Rust, it will enforce correct usage via the borrow checker - the outlive case simply will not compile.
If you do want it, you have the option to, say, heap allocate.
do_not_redeem · 2d ago
That would be a bug, so just... don't do that?
If you return a pointer to a local variable that outlives the scope, the pointer would be dangling. Does that mean we should ban pointers?
If you close over a pointer to a local variable that outlives the scope, the closure would be dangling. Does that mean we should ban closures?
tux1968 · 2d ago
Same as C, define a named function, and pass a pointer to the sorting function.
flohofwoe · 2d ago
Unlike C you can stamp out a specialized and typesafe sort function via generics though:
And what if you need to close over some local variable?
flohofwoe · 2d ago
Not possible, you'll need to pass the captured variables explicitly into the 'lambda' via some sort of context parameter.
And considering the memory management magic that would need to be implemented by the compiler for 'painless capture' that's probably a good thing (e.g. there would almost certainly be a hidden heap allocation required which is a big no-no in Zig).
pton_xd · 2d ago
As far as I know lambdas in C++ will not heap allocate. Basically equivalent to manually defining a struct, with all your captures, and then stack allocating it. However if you assign a lambda to a std::function, and it's large enough, then you may get a heap allocation.
Making all allocations explicit is one thing I do really like about Zig.
tsimionescu · 1d ago
If the lambda is a value type, you can just store whatever captures you want in the fields of this type, no need for heap allocations - they'll go on the stack just like anything else. You can even ask the user to explicitly specify which variables to capture, like in C++ lambdas, to be very explicit about the size of the lambda structure.
andrepd · 2d ago
Why would capturing require a heap allocation? Neither Rust nor C++ do this.
flohofwoe · 2d ago
You need to store the captured data somewhere if the lambda is called after the outer function returns. AFAIK C++ (or rather std::function) will heap-allocate if the capture size goes above some arbitrary limit (similar to small-string optimizations in std::string). Not sure how Rust handles this case, probably through some "I can't let you do that, Dave" restrictions ;)
steveklabnik · 2d ago
The trick is that “if”. Rust won’t ever automatically heap allocate, but if your closure does get returned to a place where the capture would be dangling, it will fail to compile. You can then choose to heap allocate it if you wish, or do something else instead.
Heap allocating them is fairly rare, because most usages are in things like combinators, which have no reason to enlarge their scope like that.
andrepd · 2d ago
But that's not a closure, a closure/lambda is not an std::function. It's its own type, basically syntactic sugar for a struct with the captures vars and operator().
Of course if you want to store it on a type-erased container like std::function then you may need to heap allocate. Rust's equivalent would be a Box<dyn Fn>.
nine_k · 2d ago
...or escape analysis and lifetimes, but we already have Rust %)
hardwaregeek · 2d ago
Everyone agrees that "syntax doesn't matter", but implicit in that is "syntax doesn't matter, so let's do what I prefer". So really, syntax does matter. Personally I prefer the Rust/Zig/Go syntax of vaguely C inspired with some nice fixes, as detailed in the post. Judging by the general success of that style, I do wonder if more functional languages should consider an alternative syntax in that style. The Haskell/OCaml concatenative currying style with whitespace is elegant, but sufficiently unfamiliar that I do think it hurts adoption.
After all, Rust's big success is hiding the spinach of functional programming in the brownie of a systems programming language. Why not imitate that?
nromiun · 2d ago
That saying never made any sense to me either. After all syntax is your main interface to a language. Anything you do has to go through the syntax.
Some people say the syntax just kind of disappears for them after some time. That never seems to happen with me. When I am reading any code the syntax gets even more highlighted.
brabel · 2d ago
I always thought that languages (or at least editors) should allow the user to use whatever syntax they want!
It's possible. Just look at Kotlin and Java... you can write the exact same code in both, even if you discount the actual differences in the languages. It's not hard to see how you could use Haskell, or Python syntax, to write the exact same thing. People don't like the idea too much because they think that makes it sharing code and reading code together harder, but I don't buy that because at least I myself, read code by myself 99% of the time, and the few times I read code with someone else, I can imagine we can just agree quite easily on which syntax to use - or just agree to always use the "default" syntax to avoid disagreements.
I dream to one day get the time to write an editor plugin that lets you read and write in any syntax... then commit the code in the "standard" syntax. How hard would that be?!
tmtvl · 2d ago
One of the reasons why I use Lisp is because the syntax helps me keep my mind organised. C-style syntax is just too chaotic for me to handle.
hinkley · 2d ago
People who do a lot of deep work on code, particularly debugging and rearchitecting, tend to have opinions about syntax and code style that are more exacting. As one of those people I’ve always felt that the people working the code the hardest deserve to have an outsized vote on how to organize the code.
Would that potentially reduce throughput a bit? Probably. But here’s the thing: most management notice how the bad situations go more than the happiest path. They will kick you when you’re down. So tuning the project a bit for the stressful days is like Safety. Sure it would be great to put a shelf here but that’s where the fire extinguishers need to go. And sure it would be great not interrupting the workday for fire safety drills, but you won’t be around to complain about it if we don’t have them. It’s one of those counterintuitive things, like mise en place is for a lot of people. Discipline is a bit more work now for less stress later.
You get more predictable throughput from a system by making the fastest things a little slower to make the slow things a lot faster. And management needs predictability.
brabel · 2d ago
If you like C-like syntax and want a functional language that uses it, try Gleam: https://gleam.run/
Quite lovely looking code.
fn spawn_greeter(i: Int) {
process.spawn(fn() {
let n = int.to_string(i)
io.println("Hello from " <> n)
})
}
There's also Reason, which is basically OCaml (also compiles to JS - funnily enough, Gleam does that too.. but the default is the Erlang VM) with C-like syntax: https://reasonml.github.io/
akkad33 · 4h ago
All of these examples look weird and unreadable to me. Rust borrows from decades of ML language syntax. I don't think that can go wrong. Not sure where the Zig choices come from but they seem to be there just to look "different"
Twey · 2d ago
> I think Kotlin nails it: val, var, fun. Note all three are monosyllable, unlike const and fn!
At least in my pronunciation, all five of those are monosyllabic! (/kQnst/, /f@n/).
(Nice to see someone agree with me on minimizing syllable count, though — I definitely find it easier to chunk[1] fewer syllables.)
The use of Ruby block parameters for loops in a language without first-class blocks/lambdas is a bit weird to me. I might have preferred a syntax that looks more like normal variable binding.
the__alchemist · 2d ago
I wish Zig had lovely vector, quaternion, matrix etx syntax. The team's refusal to add operator overloading will prevent this.
flohofwoe · 2d ago
You don't need operator overloading for vector and matrix math, see pretty much all GPU languages. What Zig is missing is a complete mapping of the Clang Extended Vector and Matrix extensions (instead of the quite limited `@Vector` type):
Agreed, their reason for not allowing it is weird. No hidden overloading?
OK make it explicit then: #+, #/ would be fine.
Cyph0n · 2d ago
That would open a can of worms, because then the next thing would use a different symbol. AFAIK, Scala had a huge issue with random symbols polluting code readability.
renox · 1d ago
That's not a reasonable objection, each proposal must be evaluated on its own, not on other non existing future proposal.
losvedir · 2d ago
> Note all three are monosyllable, unlike const and fn!
I'm not sure if I'm parsing this right, but is the implication that "const" is not monosyllabic? It certainly is for me. How else do people say it? I get "fn" because people might say "eff enn" but I don't see what the equivalent for const would be.
culebron21 · 2d ago
I've been seeing articles on Zig for the last 2 years, and was interested, but it seems the language community is too far from my area of interest -- data and geospatial, and the tools in my sphere in Zig aren't mature enough. E.g. to parse a CSV, you have to use very simple packages or just use a tokenizer and parse the tokens yourself.
IshKebab · 2d ago
> As Zig has only line-comments
Such a good decision. There's no reason to use block comments in 2025.
MrResearcher · 2d ago
It's still not clear to me how you can make two comptime closures with different contents and pass those as a functor into the same function. It needs to have a sort of VTable to invoke the function, and yet since the contents are different, the objects are different, and their deallocation will be different too.
Defining VTable in zig seems to be a pretty laborious endeavor, with each piece sewn manually.
zoogeny · 2d ago
There was a recent Zig podcast where Andrew Kelley explicitly states that manually defining a VTable is their solution to runtime polymorphism [1]. In general this means wrapping your data in a struct, which is reasonable for almost anything other than base value types.
Been saying this, and I find it strange how few agree.
cyber1 · 1d ago
At first glance, when I looked through the Zig reference, I didn’t like a lot about its syntax (though syntax isn’t the most important thing for me). But when I tried writing in it, I changed my mind - it’s a concise and convenient language with a very low entry barrier. It feels like Go, and with some C experience, you can quickly start writing functional stuff.
ivanjermakov · 2d ago
> Almost always there is an up-front bound for the number of iterations until the break, and its worth asserting this bound, because debugging crashes is easier than debugging hangs.
Perhaps in database systems domain yes, but in everything else unconditional loop is meant to loop indefinitely. Think event loops, web servers, dynamic length iterations. And in many cases `while` loop reads nicer when it has a break condition instead of a condition variable defined outside of the loop.
Y_Y · 2d ago
> C uses a needlessly confusing spiral rule
Libellous! The "spiral rule" for C is an abomination and not part of the language. I happen to find C's type syntax a bit too clever for beginners, but it's marvellously consistent and straightforward. I can read types fine without that spiral nonsense, even if a couple of judicious typedefs are generally a good idea for any fancy function types.
Twey · 2d ago
> fancy function types
If the syntax were straightforward, plain old function types wouldn't be ‘fancy’ :)
Y_Y · 2d ago
Ambiguous parse. I don't mean that all function types are fancy, I mean that some are fancy (when you have a function pointers in the arguments or return value, or several layers of indirection), and that those which are fancy are helped by typedefs.
Twey · 2d ago
I maintain — they shouldn't be fancy. ‘Fancy’ is the mark of something your language doesn't support well. They can be _long_, and that's often worth breaking up, but if something is ‘fancy’ it's because it's not clear to read, so you should attach some identifier to it that explains what it's supposed to mean. That's significantly, though not exclusively, a function (ha) of the syntax used to express the concept.
If you're working with Roman numerals, division is a very fancy thing to do.
Y_Y · 2d ago
Fair enough. I think it qualifies as "essential complexity" and in my limited experience it's not a common use case and so doesn't make sense to optimize for.
In fact in my academic and professional career most of the highly "functional" C that I've come across has been written by me, when I'd rather amuse myself than make something readable.
It (this particular example, of function pointer syntax) is absolutely just incidental complexity, though. E.G. Haskell
(a -> b) -> c -> d
becomes C
D (*f(B (*)(A)))(C)
and it's no surprise that the former is considered much less fancy than the latter.
Of course it's not common — because the language makes it painful :) The causation is the other way around. We've seen in plenty of languages that if first-class functions are ergonomic to use then people use them all over the place.
Y_Y · 1d ago
Without curring and closures it certainly will be more painful!
I might write the equivalent signature,
D f(A, B, C)
and then reorganize things to just pass f around, or make a struct if you really want to bake in your first function.
Twey · 1d ago
Right, due to the complexity of the syntax one of the most sensible things to do in C if you're faced with a problem that maps naturally to higher-order functions is to reframe the problem so that the solution doesn't use higher-order functions — basically doing a compilation step yourself. D (*f(B (*)(A)))(C) is definitely a fancy type, after all :)
throwawaymaths · 2d ago
It's not context free though.
Rusky · 2d ago
It is context free, just ambiguous.
melodyogonna · 2d ago
Very interesting, Zig seems really nice. There was a severe lack of resources when I tried to get into it few years ago, it is nice to see that situation improving in real time.
The part about integer literal is similar to Mojo, with a comp-time type that has to be materialized to another type at runtime. In Mojo though this can be done implicitly so you don't need explicit casting.
lvl155 · 2d ago
Zig is just fun to write. And to me, it’s actually what I wish Rust was like. Rust is a great language, and no one’s going to argue that point but writing Zig for the first time was so refreshing. That said, Rust is now basically default for systems and Zig came too late.
throwawaymaths · 2d ago
never underestimate second mover advantage. if zig gets static borrow checking, it would be amazing
gf000 · 2d ago
I really doubt a borrow checker could fit with zig's design goals, and you can't just add it after the fact.
Weird the author finds it lovely and compares to Kotlin, but doesn't find Kotlin superior. Kotlin invested heavily in a really nice curly brace syntax. It is actually the nicest out there. In every point the author makes, it feels like Kotlin did it the same or better. For example:
1. Integer literals. "var a = 1" doesn't work, seems absurd. In Kotlin literals do have strong types, but coercion is allowed when defining variables so "var a = 1" works, and "var a: Long = 1" works even though you can write a literal long as 1L. This means you can write numbers to function parameters naturally.
2. Multi-line string literals. OK this is a neat idea, but what about copy/paste? In Kotlin you can just write """ .. """.trimIndent() and then copy paste some arbitrary text into the string, the indent will be removed for you. The IDE will also help with this by adding | characters which looks more natural than \\ and can be removed using .trimMargin(), only downside is the trimming is done at runtime but that could easily be fixed without changing the language.
3. Record literals. This syntax is called lovely because it's designed for grep, a properly funded language like Kotlin just uses named kwargs to constructors which is more natural. There's no need to design the syntax for grep because Kotlin is intended to be used with a good IDE that can answer this query instantly and precisely.
4. Function syntax. "fn foo(a: i32) i32 {}" seems weird. If the thing that has a type and the type are normally separated by a : then why not here? Kotlin does "fun foo(a: Int): Int {}" which is more consistent.
5. Locals. Agree with author that Kotlin nails it.
6. Not using && or ||, ok this one Zig wins, the Zig way is more consistent and reads better. Kotlin does have `and` and `or` as infix operator functions, but they are for the bitwise operations :(
7. Explicit returns. Kotlin supports blocks that return values and also doesn't need semicolons, so not quite sure what the tradeoff here is supposed to be about.
8. Loops being expressions is kinda cool but the Kotlin equivalent of his example is much easier to read still: "val thing = collection.first { it.foo > bar }". It compiles to a for loop due to the function inlining.
9. Generics. Zig's way seems primitive and unnecessarily complex. In Kotlin it is common to let the compiler infer generics based on all available information, so you can just write "someMethod(emptyList())" and emptyList<T>() infers to the correct type based on what someMethod expects.
Overall Zig looks like a lot of modern languages, where they are started as a hobby or side project of some guy and so the language is designed around whatever makes implementing the compiler most convenient. Kotlin is unusual because it doesn't do that. It was well funded from the start, so the syntax is designed first and foremost to be as English-like and convenient as possible without leaving the basic realm of ordinary curly-brace functions-and-oop style languages. It manages to be highly expressive and convenient without the syntax feeling overly complex or hard to learn.
lenkite · 2d ago
> There's no need to design the syntax for grep because Kotlin is intended to be used with a good IDE that can answer this query instantly and precisely.
On large projects its still cheaper and faster to grep from the CLI than to use Intellij IDE search. Esp if you wish to restrict search to subsets of dirs.
Command-line greppability is a serious use case.
Zig is also 5-10x faster to compile than Kotlin.
On function syntax, I agree with you. It was a mistake not to use fn:ReturnType. It has destroyed future lambdas.
brabel · 2d ago
> On large projects its still cheaper and faster to grep from the CLI than to use Intellij IDE search. Esp if you wish to restrict search to subsets of dirs.
You must never have used Intellij to say that... it hurts me to hear this. If I catch a developer "grepping" for some type in the CLI, I will sit down with them for a few hours explaining how to use an IDE and how grep is just dumb text search without any of the semantic understanding of the code that an IDE has, and they should never do that again.
EDIT: IntelliJ is better than grep even at "free text" search... Much better as it will show you the results as you type, extremely fast, and lets you preview them and see their "surrounding code"... and you can then choose to navigate to a place instantly... and yes, you can scope the search to a particular directory if you want... if you can't see how this is miles superior to CLI grep, then there's no use arguing as you've made up your mind you just love being in the CLI for no actual rational reason.
lenkite · 2d ago
Umm..I _am_ talking about the free text search. Miles superior is NOT miles faster when you want speed. You need not change tabs, fiddle laboriously with the finicky scope drop-down and create custom scopes. Instead you execute an ripgrep command with filter directories (or load alias/from history), pipe to neovim and get auto-preview of results, including surrounding preview code. You don't have to load a huge project and wait for looong code-indexing.
if you can't see how this is miles superior to IDE grep (esp when exploring a number of large projects), then there's no use arguing as you've made up your mind you just love being in the IDE for no actual rational reason.
brabel · 2d ago
> Instead you execute an ripgrep command with filter directories (or load alias/from history), pipe to neovim
Talk about moving the goal posts.
lenkite · 2d ago
> Talk about moving the goal posts.
Hey, you are the one who moved the goal posts fist by bringing up use-cases like surrounding preview. You can use ripgrep -> fzf -> bat in one single command if you want to stick to the CLI for fast search previews. But I personally like neovim's comfort better and it is extremely fast to load compared to Intellij. (Not to mention, you can keep feeding the quickfix list more search results from other places)
johnisgood · 2d ago
He said cheaper and faster. It takes 5-10 minutes for IntelliJ to start up properly for me, and doing anything in it is just too slow. (rip)grep is way faster.
Yes yes, I need a better PC. (rip)grep would still be faster, however, but I would use the IDE.
qcnguy · 1d ago
It sounds totally broken. IntelliJ starts in a few seconds for me. 10 minutes is crazy, maybe your system has no free RAM at all when you try that.
johnisgood · 1d ago
I have 8 GB only, so yeah, no RAM when I have a browser open. And by startup I am referring to background indexing, project initialization, etc. on a large enough codebase.
brabel · 1d ago
Normally, the IDE is open at all times when you're coding. But if you don't code all the time, I can see how you may prefer to avoid IntelliJ and I would also do that if I was just searching for strings.
johnisgood · 1d ago
IntelliJ is unfortunately very sluggish for me either way, and as of now, if it is open, I cannot do anything else on the PC, which means it is only open when I am actively coding, but even then, it is just so slow that I would rather not.
On the other hand, VSCodium and vim / emacs are always open, at the same time. But I do not like coding in Java / Kotlin without IntelliJ, which I do for some work.
Honestly, a better PC would solve this issue.
brabel · 1d ago
I completely understand. But I suppose most developers, specially in the USA (where salaries are astronomicals) and even Europe (where I am, most top-of-the-line laptops are affordable to most devs here) it's not a problem, but I too have a low-end Mac (which would be an expensive machine in some countries!) where IntelliJ doesn't run so well, as you mention. In those cases, I use emacs which has similar "grep" functionality. What I was arguing against was just doing it "only" in the CLI. You will spend hours for something that should be minutes!! But even the guy who originally said he does it on the CLI admited he's actually just calling it from the CLI, but using an IDE (if you allow me to call Neovim an IDE) to go through the results... which is basically the poor man way to do what IntelliJ does (no offence meant).
johnisgood · 1d ago
Thanks!
FWIW in VSCodium, I have one or more terminals open, and sometimes I would (rip)grep. Not always, but sometimes, when I see there is use for it. I am used to the quick output of the tool and sometimes that is all I need.
johnisgood · 2d ago
It is not just syntax that matters. For one I dislike JVM-based languages. I still write it sometimes for work.
qcnguy · 2d ago
There is also Kotlin/Native, but yes, I am only talking about syntax here.
Tmpod · 1d ago
> only downside is the trimming is done at runtime but that could easily be fixed without changing the language.
trimMargin and trimIndent (as well as other operations like concatenation) actually get handled at compile time if the string isn't interpolated.
WalterBright · 2d ago
const x: i32 = 92;
D has less syntax:
const int x = 92;
Just for fun, read each declaration out loud.
IshKebab · 2d ago
"less syntax" isn't necessarily better. The goal isn't to have as few characters as possible.
johnisgood · 2d ago
True, it is subjective. I prefer C, Go, PHP, ... and OCaml, Erlang, Elixir, Perl, and sometimes Ada and Common Lisp.
I like the syntaxes of each, although Ada is too verbose to me, and with Factor and Common Lisp I have a skill issue.
WalterBright · 2d ago
I agree. I'm not a fan of minimized syntax (see Haskell). But I am a fan of easy to read syntax!
tmtvl · 2d ago
If it were we would all be using APL.
WalterBright · 2d ago
Zig:
fn ArrayListType(comptime T: type) type {
D:
T ArrayListType(T)() {
do_not_redeem · 2d ago
Why is omitting the fact that T is a type useful? T could be a normal value too.
This reminds me of C in the 1970s where the compiler assumed every typo was a new variable of type int. Explicit is good.
WalterBright · 2d ago
> Why is omitting the fact that T is a type useful?
It's the default, because most templates are templated on types. If you want a constant int as part of the template type,
ArrayListType(int I)
> This reminds me of C in the 1970s where the compiler assumed every typo was a new variable of type int
I think you're referring to function declarations without prototypes. D's syntax does not suffer from that issue.
BTW,
T func(T)(T x) { return x + 1; }
is a declaration of a "function template". The first parameter list consists of the template parameters, and are compile time types and constants. The second parameter list is the conventional function parameter list. If the first parameter list is omitted, then it's a function.
dpassens · 2d ago
I don't know D but shouldn't that be
struct ArrayListType(T) {
?
brabel · 2d ago
Walter is showing the equivalent function declaration... you will eventually create a generic type as you say, but in the Zig example, that was a function, not a struct.
dpassens · 2d ago
If my understanding of D's template syntax is correct, then Walter is showing a the declaration of a function called ArrayListType which is generic over T and returns a T. The original Zig code returns the struct type itself, so it is functionally equivalent to my example, provided I understood how D templates work.
brabel · 2d ago
The Zig code returns any `type`, it's impossible to say what that is without looking at the implementation. It can be different types completely depending on the comptime arguments.
But I agree it probably returns a struct type.
Assuming that's the case, you're right and the equivalent would be:
Zig:
fn ArrayListType(comptime T: type) type {
D:
ArrayList!T ArrayListType(T)() {
But now the D version is more specific about what it returns, so it's still not exactly equivalent.
dpassens · 2d ago
No, you misunderstand. The function doesn't return any type, it returns _a_ type. Types are values in Zig and returning them from function is how generics are implemented.
brabel · 1d ago
I know how Zig works. `type` is some type the function will return, you must look at the implementation to know what actually got returned, given the comptime arguments given to it by the caller (as I already mentioned). Where is the misunderstanding??
dpassens · 1d ago
Well, your example seems to do something completely different, which is return an ArrayList!T rather than a type.
brabel · 1d ago
It achieves the same result in practice. The reason Zig needs to return a type is because it lacks a way to represent `T!A` where T is a parameterized type, and A is the parameter. In this case, that would've been better because you would be able to tell exactly what the type being returned was.
If you must return different types depending on the argument in D, it's also possible.
Here's a silly example:
struct ArrayList(T, alias capacity) {
private T[capacity] array;
private uint _length;
uint length() const => _length;
T get(uint index) const => array[index];
void add(T t) {
array[_length++] = t;
}
}
struct EmptyList(T) {
uint length() const => 0;
}
/// This will return a different type depending on the length argument,
/// which is like a Zig comptime argument.
/// We cannot return the type itself, but the result is very similar.
auto createArrayList(T, alias length)() {
static if (length == 0) {
return EmptyList!T();
} else {
return ArrayList!(T, length)();
}
}
void main()
{
import std.stdio;
auto empty = createArrayList!(int, 0);
writeln(empty);
auto list = createArrayList!(int, 2);
list.add(5);
list.add(6);
writeln(list);
}
Result:
EmptyList!int()
ArrayList!(int, 2)([5, 6], 2)
dpassens · 13h ago
It's now too late to edit my original comment but I've skimmed your code a little too fast and now I see that you do understand that Zig works with reified types. But in that case, why conflate too very different things? Returning values of different types is not the same as returning types.
Regardless, we are now very far removed from the original code I was commenting on, which doesn't use auto to return different types.
dpassens · 17h ago
You still misunderstand the type type. "type" does not mean "generic over all types" but that you're returning a reified type. The original Zig function does not return an ArrayList value, it returns the ArrayList type.
WalterBright · 2d ago
Zig:
const std = @import("std");
D:
import std;
brabel · 2d ago
I think this is not equivalent because in D, this imports all symbols in the package `std` while in Zig, you just get a "struct" called `std`. I think the equivalent D is:
import std=std;
WalterBright · 1d ago
What I wrote is equivalent to the zig declaration. Google says:
const my_module = @import("my_module.zig");
This allows you to access pub (public) declarations from my_module.zig through the my_module identifier.
brabel · 1d ago
Yes, but how is that equivalent?
Zig:
const std = @import("std");
pub fn main() !void {
// std is like a namespace here
std.debug.print("Hello, World!\n", .{});
}
D:
import std.stdio;
void main()
{
// no namespace here, "writeln" and everything else in "std.stdio" is imported
writeln("Hello, World!");
}
Having @ and .{ } all over the place is hardly lovely, as is having modules like JavaScript's CJS.
Akronymus · 2d ago
It's especially bad on a qwertz keyboard as well.
flohofwoe · 2d ago
That's why "real programmers" use English keyboard layout regardless of the physical keyboard ;)
Curly brace syntax would never have been invented in Europe (case in point: Python and Pascal).
Akronymus · 2d ago
Oh for sure. I am using qwerty myself. And my fav language (f#) has relatively few curly braces.
christophilus · 2d ago
F# is wonderful. I wish someone would make an F# that compiled as fast as OCaml or Go and which had Go’s standard library and simple tooling.
pjmlp · 2d ago
.NET ecosystem is only matched by Java, and Native AOT exists, even if there are some issues with printf, due to its current implementation.
pjmlp · 2d ago
Real programmers deliver business value, regardless of what keyboard they have at their disposal.
Just like great musicians make the difference in the band, regardless of the instruments scattered around the studio.
lenkite · 2d ago
JSON is everywhere nowadays - How can one complain about curly braces {} ? EU programmers would have already mapped {} after their first REST API.
hinkley · 2d ago
I’ve been using Dvorak for 28 years and I still fat finger punctuation daily.
minitech · 2d ago
There’s nothing wrong with the CommonJS approach except that it’s not designed for static analysis (and whether that was really an issue is debatable). In Zig, it’s compile-time.
pjmlp · 1d ago
For starters being text based for what is supposed to be a systems language, that should support binary distribution.
minitech · 1d ago
Supporting binary distribution seems unrelated to the ways in which it’s “like JavaScript’s CJS”, especially in the context of this post about syntax.
pjmlp · 1d ago
Not really, because I expect to also have binary distribution of modules, as in systems languages like Modula-2, Mesa, Object Pascal/Turbo/Delphi, D, Ada,....
fallow64 · 2d ago
Every file is an implicit struct, so importing a module is just importing a struct with static members.
Yeah, which is basically how requires() kind of works.
zabzonk · 2d ago
In my experience, everyone finds the syntax of their favourite language lovely - I love (mostly) C++.
aeonik · 2d ago
Not me!
I don't like the syntax of Lisps, with the leading parenthesis to begin every expression.
I use it anyway because it's so useful and powerful. And I don't have any better ideas.
zabzonk · 2d ago
> leading parenthesis to begin every expression
well, that's not really what it is doing - it is saying apply this function to these parameters.
brabel · 2d ago
No, it's an s-expression... it may be a function call, but it may also be a macro, and inside a macro it may be a parameter list or a number of other things.
ben-schaaf · 2d ago
As someone who works almost exclusively in C++, the whole "most vexing parse" makes the syntax indefensible.
zabzonk · 2d ago
In my experience, people simply go "Doh, of course!" or don't write the wrong code in the first place. It has never caused me any real problems.
For those of you that may be curious, or not know C++, in C++ this:
Obj x();
is a declaration of a function called x that returns an Obj. Whereas this:
Obj x;
defines (possibly, depending on context) an instance of the type Obj called x.
Most people get over this pretty quickly.
If that is your main complaint about a language then the language doesn't have too many problems. Not that I'm suggesting that C++ doesn't have more serious problems.
guidopallemans · 2d ago
I don't like how Python does lambdas, its indentation-based blocks, how there's both ' and ", I could go on.
jeltz · 2d ago
I am an exception then. Rust may be my favourite language but the syntax is pretty awful and one of its biggest weaknesses. I also love Ruby but I am pretty meh about its syntax.
zabzonk · 2d ago
I don't think many people would start to use a language unless the found the syntax at least a little simpatico - but I guess they could be drawn by the semantics it provides.
sampullman · 2d ago
I (almost) don't consider syntax at all when considering a language, just whether it's suitable for the task.
The exception is languages with syntax that require a non-standard keyboard.
jeltz · 2d ago
I mostly chose languages due to semantics.
wolvesechoes · 1d ago
So Zig is a new darling, eh?
I sometimes have an impression that the main purpose of all these new languages is to write blog posts about them. Even if they call themselves general-purpose, they are mostly blog-purpose.
yegle · 2d ago
> As Zig has only line-comments, this means that \n is always whitespace.
Do I read this correctly that it replaces `\n` at the end of the line with a whitespace? CJK users probably won't be happy with the additional whitespaces.
throwawaymaths · 2d ago
that's not correct.
do_not_redeem · 2d ago
Since we're talking syntax... it's mildly infuriating that the zig parser is not smart enough to understand expressions like `const x=a()orelse b();`. You have to manually add a space before `orelse` -- but isn't that what `zig fmt` is for? I have RSI and it's maddening having to mash the arrow keys and add/remove whitespace until the parser is happy.
I've heard the argument that people might confuse binary operators for prefix/postfix operators, but I don't buy it. Who would think an operator named `orelse` is anything but binary?
hmry · 2d ago
"Read the file, or else!" The threatening postfix operator
hiccuphippo · 2d ago
No idea why it wouldn't add the space, but you could configure your editor to always add a space after `)` and let zig fmt remove the space when not needed.
nateglims · 2d ago
The error when you accidentally pass a variable directly instead of in a .{} is also really unclear.
Western0 · 2d ago
Love Ruby and Cristal syntax ;-)
darthrupert · 2d ago
Absolutely. Kinda of the best of many worlds, because it has what looks like an indentation-based syntax but actually blocks are delimited by start and end keywords. This makes it possible (unlike, say, python) to programmatically derive correct code formatting more reliably.
And no pointless semicolons.
norir · 2d ago
From my vantage point, Zig's syntax perfectly matches the language: it is ad-hoc, whimsical and serendipitous. It is lacking in grace, elegance and compassion.
the thing that stands out to me about Zig's syntax that makes it "lovely" (and I think matklad is getting at here), is there is both minimalism and consistency to the design, while ruthlessly prioritizing readability. and it's not the kind of surface level "aesthetically beautiful" readability that tickles the mind of an abstract thinker; it is brutalist in a way that leaves no room for surprise in an industrial application. it's really, really hard to balance syntax design like this, and Zig has done a lovely and respectable job at doing so.
Zigs use of try/catch is incredible, and by far my favorite error handling of any language. I feel like it would have fit into this article.
Rather, the sort of beauty it's going for here is exactly the type of beauty that requires a bit of abstraction to appreciate: it's not that the concrete syntax is visually beautiful per se so much as that it's elegantly exposing the abstract syntax, which is inherently more regular and unambiguous than the concrete syntax. It's the same reason S-exprs won over M-exprs: consistently good often wins over special-case great because the latter imposes the mental burden of trying to fit into the special case, while the former allows you to forget that the problem ever existed. To see a language do the opposite of this, look at C++: the syntax has been designed with many, many special cases that make specific constructs nicer to write, but the cost of that is that now you have to remember all of them (and account for all of them, if templating — hence the ‘new’ uniform initialization syntax[1]).
[1]: https://xkcd.com/927/
This trade-off happens all the time in language design: you're looking for language that makes all the special cases nice _as a consequence of_ the general case, because _just_ being simple and consistent leads you to the Turing tarpit: you simplify the language by pushing all the complexity onto the programmer.
I don't really know how else to put it, but it's vaguely like a C derived spiritual cousin of Lisp with structs instead of lists.
- we have a language with a particular philosophy of development
- we discover that some concept A is awkward to express in the language
- we add a special case to the language to make it nicer
- someone eventually invents a new base language that natively handles concept A nicely as part of its general model
Lisp in some sense skipped a couple of those progressions: it had a very regular language that didn't necessarily have a story for things that people at the time cared about (like static memory management, in the guise of latency). But it's still a paragon of consistency in a usable high-level language.
I agree that it's of course not correct to say that Zig is a descendent or modern equivalent of Lisp. It's more that the virtue that Lisp embodies over all else is a universal goal of language design, just one that has to be traded off against other things, and Zig has managed to do pretty well at it.
Zig comptime operates a lot like very old school Lisp FEXPRS before the Lisp intelligentsia booted them out because FEXPRS were theoretically messy and hard to compile.
Visually-heterogeneous syntaxes, for all of their flaws, are easier to read because it's easier for the human brain to pattern-match on distinct features than indistinct ones.
I'm definitely an outlier on this given the direction all syntactically C-like new languages have taken, but I have the opposite preference. I find that the most common reason I go back to check a variable declaration is to determine the type of the variable, and the harder it is to visually find that, the more annoyed I'm going to be. In particular, with statically typed languages, my mental model tends to be "this is an int" rather than "this is a variable that happens to have the type 'int'".
In Rust, in particular, this leads to some awkward syntactic verbosity, because mutable variables are declared with `let mut`, meaning that `let` is used in every declaration. In C or C++ the type would take the place of that unnecessary `let`. And even C (as of C23) will do type inference with the `auto` keyword. My tendency is to use optional type inference in places where needing to know the type isn't important to understand the code, and to specify the type when it would serve as helpful commentary when reading it back.
In my opinion the `let` is not so unnecessary. It clearly marks a statement that declares a variable, as opposed to other kind of statements, for example a function call.
This is also why C++ need the "most vexing parse" ambiguity resolution.
From a parser perspective, it’s easier to go name first so you can add it to the AST and pass it off to the type determiner to finish the declaration. So I get it. In typescript I believe it’s this way so parsers can just drop types all together to make it compatible with JavaScript (it’s still trivial to strip typing, though why would you?) without transpiling.
In go, well, you have even more crazier conventions. Uppercase vs lowercase public vs private, no inheritance, a gc that shuns away performance minded devs.
In the end, I just want a working std library that’s easy to use so I can build applications. I don’t care for:
This is the kind of abuse of the type system that drives me bonkers. You don’t have to be clever, just export a function. I don’t need a type to represent every state, I need intent.Hover the mouse cursor over it. Any reasonable editor will show the type.
> In Rust, in particular, this leads to some awkward syntactic verbosity, because mutable variables are declared with `let mut`, meaning that `let` is used in every declaration.
Rust is very verbose for strange implementation reasons... namely to avoid parse ambiguities.
> In C or C++ the type would take the place of that unnecessary `let`.
OTOH, that means you can't reliably grep for declarations of a variable/function called "foo". Also consider why some people like using
style. This was introduced because of template nonsense (how to declare a return type before the type parameters were known), but it makes a lot of sense and makes code more greppable. Generic type parameters make putting the return type at the front very weird -- reading order wise.Anyhoo...
> Hover the mouse cursor over it. Any reasonable editor will show the type.
That applies to most of us, obviously, but in this context we're talking about Zig. Zig's lead developer, Andrew Kelley, programs in Vim with no autocomplete or mouse support.
Even though I sometimes use editors with these features, I find it frustrating when languages seem to be designed in such a way that presumes their availability. I found Rust particularly bad about this, for example.
1. Allows type inference without a hacky 'auto' workaround like c++ and 2. Is less ambiguous parsing wise. I.e. when you read 'MyClass x', MyClass could be a variable (in which case this is an error) or a type; it's impossible to know without context!
If it isn't obvious, the problem is that you can't indent them properly because the indentation becomes part of the string itself.
Some languages have magical "removed the indent" modes for strings (e.g. YAML) but they generally suck and just add confusion. This syntax is quite clear (at least with respect to indentation; not sure about the trailing newline - where does the string end exactly?).
Snippet from my shader compiler tests (the `\` vs `/` in the paths in params and output is intentional, when compiled it will generate escape errors so I'm prodded to make everything `/`):
A\n\tsimple\n\t\tformatted\n\t\t\tstring\n\t
If you wanted it without the additional indentation, you’d need to use a function to strip that out. Typescript has dedent which goes in front of the template string, for example. I guess in Zig that’s not necessary which is nice.
I would so much rather read and write:
than In this particular example, zig doesn't look that bad, but for longer strings, I find adding the // prefix onerous and makes moving strings around different contexts needlessly painful. Yes, I can automatically add them with vim commands, but I would just rather not have them at all. The trailing """ is also unnecessary in this case, but it is nice to have clear bookends. Zig by contrast lacks an opening bracket but requires a closing bracket, but the bracket it uses `;` is ambiguous in the language. If all I can see is the last line, I cannot tell that a string precedes it, whereas in my example, you can.Here is a simple way to implement the former case: require tabs for indentation. Parse with recursive descent where the signature is
Multiline string parsing becomes a matter of bumping the indent parameter. Whenever the parser encounters a newline character, it checks the indentation and either skips it, or if is less than the current indentation requires a closing """ on the next line at a reduced indentation of one line.This can be implemented in under 200 lines of pure lua with no standard library functions except string.byte and string.sub.
It is common to hear complaints about languages that have syntactically significant whitespace. I think a lot of the complaints are fair when the language does not have strict formatting rules: python and scala come to mind as examples that do badly with this. With scala, practically everyone ends up using scalafmt which slows down their build considerably because the language is way too permissive in what it allows. Yaml is another great example of significant whitespace done poorly because it is too permissive. When done strictly, I find that a language with significant whitespace will always be more compact and thus, in my opinion, more readable than one that does not use it.
I would never use zig directly because I do not like its syntax even if many people do. If I was mandated to use it, I would spend an afternoon writing a transpiler that would probably be 2-10x faster than the zig compiler for the same program so the overhead of avoiding their decisions I disagree with are negligible.
Of course from this perspective, zig offers me no value. There is nothing I can do with zig that I can't do with c so I'd prefer it as a target language. Most code does not need to be optimized, but for the small amount that does, transpiling to c gives me access to almost everything I need in llvm. If there is something I can't get from c out of llvm (which seems highly unlikely), I can transpile to llvm instead.
I would have probably gone with ` or something.
.. it is using \\
It’s some sort of mental glitch that a number of people fall into and I have absolutely no idea why.
It used to grate on my nerves to hear people say, e.g. "H T T P colon backslash backslash yahoo dot com".
But I think they always typed forward slash, like they knew the correct slash to use based on the context, but somehow always spoke it in DOSish.
Choice of specific line-start marker aside, I think this is the best solution to the indented-string problem I've seen so far.
> In text blocks, the leftmost non-whitespace character on any of the lines or the leftmost closing delimiter defines where meaningful white space begins.
From https://blogs.oracle.com/javamagazine/post/text-blocks-come-...
It's not a bad option but it does mean you can't have text where every line is indented. This isn't uncommon - e.g. think about code generation of a function body.
You just put the ending """ where you want it relative to the content.
I've only seen two that do: the Zig approach, and a postprocessing ‘dedent’ step.
spoken as someone who found the syntax offensive when I first learned it.
Usually, representing multiline strings within another multiline string requires lots of non-trivial escaping. This is what this example is about: no escaping and no indent nursery needed in Zig.
Makes cut 'n' paste embedded shader code, assembly, javascript so much easier to add, and more readable imo. For something like a regular expressions I really liked Golang's back tick 'raw string' syntax.
In Zig I find myself doing an @embedFile to avoid the '\\' pollution.
1. from the user's point of view, you can now have multiline string literals that are properly indented based on their surrounding source code, without the leading spaces being treated as part of the string
2. from an implementation point of view having them parsed as individual lines is very elegant, it makes newline characters in the code unambiguous and context independent. they always break up tokens in the code, regardless of whether they are in a string literal or not.
People are having trouble distinguishing between '//' and '\\'.
Zig has some nice things going on but somehow code is really hard to read, admitting its a skill issue as im not that versed in zig.
Of course the question is whether the leading dot in '.{' could be omitted, and personally I would be in favour of that. Apparently it simplifies the parser, but such implementation details should get in the way of convenience IMHO.
And then there's `.x = 123` vs `x: 123`. The Zig form is copied from C99, the Rust form from Javascript. Since I write both a lot of C99 and Typescript I don't either form (and both Zig and Rust are not even close to the flexibility and convenience of the C99 designated initialization syntax unfortunately).
Edit: fixed the Rust struct init syntax.
https://github.com/ziglang/zig/issues/5038
So the explanation of a dot standing in for a type doesn't make sense in the long run.
https://github.com/ziglang/zig/issues/5038#issuecomment-2441...
A language hostile to LSP/intellisense.
IMHO Odin got it exactly right. For a variable with explicit type:
...or for inferred type: ...and the same for constants: ...but I think that doesn't fit into Zig's parser design philosophy (e.g. requiring some sort of keyword upfront so that the parser knows the context it's in right from the start instead of delaying that decision to a later time).Think I’d rather do the Point{} syntax.
No comments yet
I think it's a bit more complicated than that: AFAIK Zig consts without explicit type may be comptime_int or comptime_float, and those don't exist at runtime. Only consts with an explicit type annotation are 'runtime consts'.
Also see:
https://www.godbolt.org/z/esTr463bT
...still, I think Rust should allow to infer the type at least inside struct initialization, it would make designated init code like this a lot less noisy (Zig would suffer from the same problem if it hadn't the .{} syntax):
https://github.com/floooh/sokol-rust/blob/main/examples/texc...
...C99 is still the 'benchmark' when it comes to struct initialization:
https://github.com/floooh/sokol-samples/blob/29d5e9f4a56ae18...
Well in Rust code like this:
...I cannot write: ...even though the Rust compiler has all the type information it needs (from the 'left-hand-side').For comparison, in Zig it would look like this:
...Zig is still only halfway there compared to C99 (e.g. Zig doesn't allow designator chaining and is much less flexible for initializing nested arrays - in those areas it's closer to Rust than C).Would you take syntax like clear_value: _ { r: 0.25, g: 0.5, g: 0.75, a: 1.0 } ?? Then we're saying that we know we need to pick a type here but the type can be inferred where our underscore was, just like when we
let words: Vec<_> = "A sentence broken by spaces".split_whitespace().collect();
The indentation of the final ` """` line is what is removed from all other lines. Not the indentation of the first line. This allows the first line to be indented as well.
Cheers, and I'm glad you like it. I thought we did a really good job with that feature :-)
Haven't used much C#, but I love Scala's `.stripPrefix` and `StringContext`.
But sure, if you only compare it with Rust, it is a big improvement.
An excessively terse syntax becomes very unforgiving, when a typo is not noticed by the compiler / language server, but results in another syntactically correct but unexpected program, or registers as a cryptic error much farther downstream. Cases in point: CoffeeScript, J.
Too many symbols like ., :, @, ; etc just mess with my brain.
It's the wrong name though. In type theory, (), the type with one member, is traditionally called "Unit", while "Void" is the uninhabited type. Void is the return type of e.g. abort.
A ton of people coming to Zig are going to be coming from C and C++. void is fine.
Cool trick: some languages (e.g. TypeScript) allow `void` generics making parameters of that type optional.
This means that a function that returns an uninhabited type is statically known to not return -- since there's no way it could construct an uninhabited value for a return expression. It also means you can coerce a value of uninhabited type into any other type, because any code which recieves a value of an uninhabited type must be unreachable.
For instance, in Rust you can write the following:
Because panic! aborts the program and doesn't return, its return type is uninhabited. Thus, a panic! can be used in a context where a string is expected.> The mode VOID has a single value denoted by EMPTY.
No comments yet
- Difficult to return a value from a block. Rust treats the value of the last expression of a block as the return value of the block. Have to jump through hoops in Zig to do it with label.
- Unable to chain optional checks, e.g. a?.b?.c. Support for monadic types would be great so general chaining operations are supported.
- Lack of lambda support. Function blocks are already supported in a number of places, i.e. the for-loop block and the catch block.
This surprises me (as a C++ guy). I use lambdas everywhere. What's the standard way of say defining a comparator when sorting an array in Zig?
This is indeed a point which makes Zig inflexible.
Go has a similar function declaration, and it supports anonymous functions/lambdas.
E.g. in go, an anonymous func like this could be defined as
foo := func(x int, _ int) int { … }
So I’d imagine in Zig it should be feasible to do something like
var foo = fn(x: i32, i32) i32 { … }
unless I’m missing something?
See
https://github.com/golang/go/issues/59122
https://github.com/golang/go/issues/21498
vsSome other people have tried to explain how they prefer types before variable declarations, and they’ve done a decent job of it, but it’s the function return type being buried that bothers me the most. Since I read method signatures far more often than method bodies.
fn i32 add(…) is always going to scan better to me.
Elixir is trying something, I don’t know yet whether it will be better. But their solution is based on a decision about how to do overloading that I suspect makes for maintenance problems later. So it’s gonna have to be good to offset the consequence.
There's a little more syntax than a dedicated language feature, but not a lot more.
What's "missing" in zig that lambda implementations normally have is capturing. In zig that's typically accomplished with a context parameter, again typically a struct.
Why don't they just add lambdas?
Well, not really.
Consider lambdas in C++ (that was the perspective of the post I replied to). Before lambdas, you used functors to do the same thing. However, the syntax was slightly cumbersome and C++ has the design philosophy to add specialized features to optimize specialized cases, so they added lambdas, essentially as syntactic sugar over functors.
In zig the syntax to use an anonymous struct like a functor and/or lambda is pretty simple and the language has the philosophy to keep the language small.
Thus, no need for lambdas. There's no re-inventing anything, just using the language as it designed to be used.
Lambdas are great for convenience and productivity. Eventually they can lead to memory cycles and leaks. The side-effect is that software starts to consume gigabytes of memory and many seconds for a task that should take a tiny fraction of that. Then developers either call someone who understands memory management and profiling, or their competition writes a better version of that software that is unimaginably faster. ex. https://filepilot.tech/
If you do want it, you have the option to, say, heap allocate.
If you return a pointer to a local variable that outlives the scope, the pointer would be dangling. Does that mean we should ban pointers?
If you close over a pointer to a local variable that outlives the scope, the closure would be dangling. Does that mean we should ban closures?
https://ziglang.org/documentation/master/std/#std.sort.binar...
And considering the memory management magic that would need to be implemented by the compiler for 'painless capture' that's probably a good thing (e.g. there would almost certainly be a hidden heap allocation required which is a big no-no in Zig).
Making all allocations explicit is one thing I do really like about Zig.
Heap allocating them is fairly rare, because most usages are in things like combinators, which have no reason to enlarge their scope like that.
Of course if you want to store it on a type-erased container like std::function then you may need to heap allocate. Rust's equivalent would be a Box<dyn Fn>.
After all, Rust's big success is hiding the spinach of functional programming in the brownie of a systems programming language. Why not imitate that?
Some people say the syntax just kind of disappears for them after some time. That never seems to happen with me. When I am reading any code the syntax gets even more highlighted.
I dream to one day get the time to write an editor plugin that lets you read and write in any syntax... then commit the code in the "standard" syntax. How hard would that be?!
Would that potentially reduce throughput a bit? Probably. But here’s the thing: most management notice how the bad situations go more than the happiest path. They will kick you when you’re down. So tuning the project a bit for the stressful days is like Safety. Sure it would be great to put a shelf here but that’s where the fire extinguishers need to go. And sure it would be great not interrupting the workday for fire safety drills, but you won’t be around to complain about it if we don’t have them. It’s one of those counterintuitive things, like mise en place is for a lot of people. Discipline is a bit more work now for less stress later.
You get more predictable throughput from a system by making the fastest things a little slower to make the slow things a lot faster. And management needs predictability.
Quite lovely looking code.
There's also Reason, which is basically OCaml (also compiles to JS - funnily enough, Gleam does that too.. but the default is the Erlang VM) with C-like syntax: https://reasonml.github.io/At least in my pronunciation, all five of those are monosyllabic! (/kQnst/, /f@n/).
(Nice to see someone agree with me on minimizing syllable count, though — I definitely find it easier to chunk[1] fewer syllables.)
[1]: https://en.m.wikipedia.org/wiki/Chunking_(psychology)
The use of Ruby block parameters for loops in a language without first-class blocks/lambdas is a bit weird to me. I might have preferred a syntax that looks more like normal variable binding.
https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...
https://clang.llvm.org/docs/LanguageExtensions.html#matrix-t...
I'm not sure if I'm parsing this right, but is the implication that "const" is not monosyllabic? It certainly is for me. How else do people say it? I get "fn" because people might say "eff enn" but I don't see what the equivalent for const would be.
Such a good decision. There's no reason to use block comments in 2025.
1. https://youtu.be/x3hOiOcbgeA?si=Kb7SrhdammEiVvDN&t=7620
Been saying this, and I find it strange how few agree.
Perhaps in database systems domain yes, but in everything else unconditional loop is meant to loop indefinitely. Think event loops, web servers, dynamic length iterations. And in many cases `while` loop reads nicer when it has a break condition instead of a condition variable defined outside of the loop.
Libellous! The "spiral rule" for C is an abomination and not part of the language. I happen to find C's type syntax a bit too clever for beginners, but it's marvellously consistent and straightforward. I can read types fine without that spiral nonsense, even if a couple of judicious typedefs are generally a good idea for any fancy function types.
If the syntax were straightforward, plain old function types wouldn't be ‘fancy’ :)
If you're working with Roman numerals, division is a very fancy thing to do.
In fact in my academic and professional career most of the highly "functional" C that I've come across has been written by me, when I'd rather amuse myself than make something readable.
[0] https://en.wikipedia.org/wiki/Essential_complexity
(a -> b) -> c -> d
becomes C
D (*f(B (*)(A)))(C)
and it's no surprise that the former is considered much less fancy than the latter.
Of course it's not common — because the language makes it painful :) The causation is the other way around. We've seen in plenty of languages that if first-class functions are ergonomic to use then people use them all over the place.
I might write the equivalent signature,
and then reorganize things to just pass f around, or make a struct if you really want to bake in your first function.The part about integer literal is similar to Mojo, with a comp-time type that has to be materialized to another type at runtime. In Mojo though this can be done implicitly so you don't need explicit casting.
why do you say that?
https://www.youtube.com/watch?v=ZY_Z-aGbYm8
1. Integer literals. "var a = 1" doesn't work, seems absurd. In Kotlin literals do have strong types, but coercion is allowed when defining variables so "var a = 1" works, and "var a: Long = 1" works even though you can write a literal long as 1L. This means you can write numbers to function parameters naturally.
2. Multi-line string literals. OK this is a neat idea, but what about copy/paste? In Kotlin you can just write """ .. """.trimIndent() and then copy paste some arbitrary text into the string, the indent will be removed for you. The IDE will also help with this by adding | characters which looks more natural than \\ and can be removed using .trimMargin(), only downside is the trimming is done at runtime but that could easily be fixed without changing the language.
3. Record literals. This syntax is called lovely because it's designed for grep, a properly funded language like Kotlin just uses named kwargs to constructors which is more natural. There's no need to design the syntax for grep because Kotlin is intended to be used with a good IDE that can answer this query instantly and precisely.
4. Function syntax. "fn foo(a: i32) i32 {}" seems weird. If the thing that has a type and the type are normally separated by a : then why not here? Kotlin does "fun foo(a: Int): Int {}" which is more consistent.
5. Locals. Agree with author that Kotlin nails it.
6. Not using && or ||, ok this one Zig wins, the Zig way is more consistent and reads better. Kotlin does have `and` and `or` as infix operator functions, but they are for the bitwise operations :(
7. Explicit returns. Kotlin supports blocks that return values and also doesn't need semicolons, so not quite sure what the tradeoff here is supposed to be about.
8. Loops being expressions is kinda cool but the Kotlin equivalent of his example is much easier to read still: "val thing = collection.first { it.foo > bar }". It compiles to a for loop due to the function inlining.
9. Generics. Zig's way seems primitive and unnecessarily complex. In Kotlin it is common to let the compiler infer generics based on all available information, so you can just write "someMethod(emptyList())" and emptyList<T>() infers to the correct type based on what someMethod expects.
Overall Zig looks like a lot of modern languages, where they are started as a hobby or side project of some guy and so the language is designed around whatever makes implementing the compiler most convenient. Kotlin is unusual because it doesn't do that. It was well funded from the start, so the syntax is designed first and foremost to be as English-like and convenient as possible without leaving the basic realm of ordinary curly-brace functions-and-oop style languages. It manages to be highly expressive and convenient without the syntax feeling overly complex or hard to learn.
On large projects its still cheaper and faster to grep from the CLI than to use Intellij IDE search. Esp if you wish to restrict search to subsets of dirs.
Command-line greppability is a serious use case.
Zig is also 5-10x faster to compile than Kotlin.
On function syntax, I agree with you. It was a mistake not to use fn:ReturnType. It has destroyed future lambdas.
You must never have used Intellij to say that... it hurts me to hear this. If I catch a developer "grepping" for some type in the CLI, I will sit down with them for a few hours explaining how to use an IDE and how grep is just dumb text search without any of the semantic understanding of the code that an IDE has, and they should never do that again.
EDIT: IntelliJ is better than grep even at "free text" search... Much better as it will show you the results as you type, extremely fast, and lets you preview them and see their "surrounding code"... and you can then choose to navigate to a place instantly... and yes, you can scope the search to a particular directory if you want... if you can't see how this is miles superior to CLI grep, then there's no use arguing as you've made up your mind you just love being in the CLI for no actual rational reason.
if you can't see how this is miles superior to IDE grep (esp when exploring a number of large projects), then there's no use arguing as you've made up your mind you just love being in the IDE for no actual rational reason.
Talk about moving the goal posts.
Hey, you are the one who moved the goal posts fist by bringing up use-cases like surrounding preview. You can use ripgrep -> fzf -> bat in one single command if you want to stick to the CLI for fast search previews. But I personally like neovim's comfort better and it is extremely fast to load compared to Intellij. (Not to mention, you can keep feeding the quickfix list more search results from other places)
Yes yes, I need a better PC. (rip)grep would still be faster, however, but I would use the IDE.
On the other hand, VSCodium and vim / emacs are always open, at the same time. But I do not like coding in Java / Kotlin without IntelliJ, which I do for some work.
Honestly, a better PC would solve this issue.
FWIW in VSCodium, I have one or more terminals open, and sometimes I would (rip)grep. Not always, but sometimes, when I see there is use for it. I am used to the quick output of the tool and sometimes that is all I need.
trimMargin and trimIndent (as well as other operations like concatenation) actually get handled at compile time if the string isn't interpolated.
I like the syntaxes of each, although Ada is too verbose to me, and with Factor and Common Lisp I have a skill issue.
This reminds me of C in the 1970s where the compiler assumed every typo was a new variable of type int. Explicit is good.
It's the default, because most templates are templated on types. If you want a constant int as part of the template type,
> This reminds me of C in the 1970s where the compiler assumed every typo was a new variable of type intI think you're referring to function declarations without prototypes. D's syntax does not suffer from that issue.
BTW,
is a declaration of a "function template". The first parameter list consists of the template parameters, and are compile time types and constants. The second parameter list is the conventional function parameter list. If the first parameter list is omitted, then it's a function.But I agree it probably returns a struct type.
Assuming that's the case, you're right and the equivalent would be:
Zig:
D: But now the D version is more specific about what it returns, so it's still not exactly equivalent.If you must return different types depending on the argument in D, it's also possible.
Here's a silly example:
Result:Regardless, we are now very far removed from the original code I was commenting on, which doesn't use auto to return different types.
Zig:
D: The closest to Zig in D would be:Curly brace syntax would never have been invented in Europe (case in point: Python and Pascal).
Just like great musicians make the difference in the band, regardless of the instruments scattered around the studio.
You can also do something like:
```Point.zig x: i32, y: i32,
const Self = @This(); fn add(self: Self, other: Self) Self { // ... } ```
I don't like the syntax of Lisps, with the leading parenthesis to begin every expression.
I use it anyway because it's so useful and powerful. And I don't have any better ideas.
well, that's not really what it is doing - it is saying apply this function to these parameters.
For those of you that may be curious, or not know C++, in C++ this:
is a declaration of a function called x that returns an Obj. Whereas this: defines (possibly, depending on context) an instance of the type Obj called x.Most people get over this pretty quickly.
If that is your main complaint about a language then the language doesn't have too many problems. Not that I'm suggesting that C++ doesn't have more serious problems.
The exception is languages with syntax that require a non-standard keyboard.
I sometimes have an impression that the main purpose of all these new languages is to write blog posts about them. Even if they call themselves general-purpose, they are mostly blog-purpose.
Do I read this correctly that it replaces `\n` at the end of the line with a whitespace? CJK users probably won't be happy with the additional whitespaces.
I've heard the argument that people might confuse binary operators for prefix/postfix operators, but I don't buy it. Who would think an operator named `orelse` is anything but binary?
And no pointless semicolons.