Get the top HN stories in your inbox every day.
travisgriggs
sekao
The way zig does generics is brilliant, and made me think "this is how every language should do it". One of those things that seems obvious in retrospect: just pass the types as normal parameters, and for generic data structures just represent their type as a function that receives those parameters and returns a new type. Man that's just beautiful. Only downside is, i suppose the types can never be inferred so they always need to be explicitly passed. But being explicit seems to be their thing anyway.
naasking
Zig's approach isn't unique, it's basically what dependently typed languages do. The problem is that if you don't do it right, it's too expressive and the type checker can loop forever.
Of course, this is also true of C++ templates since they're Turing complete, but it's not true of generics in most languages.
rackjack
This isn't a gripe directed at you specifically.
I've noticed that when somebody says they like a growing language (Rust, Zig, ...) for this or that feature, people often come out of the woodwork to claim that it isn't unique, that some research project did it 5 years ago, etc. etc.
First, that's not even what they were saying. They like a feature of the language, they weren't making a claim about the novelty of it.
Second, even if they did erroneously make a claim about the feature's novelty, I think the theory type of people dismiss these sorts of languages too readily. Yes, somebody did it before. But it is usually very hard to bring these features into the mainstream or even adjacent to it.
It's just annoying when we're appreciating a language and the work that went into it and somebody pops their head in and says "Ackshually, Joe Gringoff published a paper in '95 detailing that exact thing, so it isn't anything new." Like I'm trying to enjoy "Samson and Delilah", I'm not really thinking about who did that kind of lighting first, so why are you using the lack of novelty to diminish the effort put into the lighting? If you want to say, "Fun fact, Giorno Capucelli was the first one to popularize that kind of lighting!" Then that's cool, but instead these people always use these facts to diminish something else instead of enhancing it. Just let me enjoy their handiwork!
wk_end
> but it's not true of generics in most languages
not sure how true that is.
* TypeScript: https://github.com/microsoft/TypeScript/issues/14833
* Rust: https://sdleffler.github.io/RustTypeSystemTuringComplete/
* Haskell (GHC): https://mail.haskell.org/pipermail/haskell/2006-August/01835...
* Scala: https://michid.wordpress.com/2010/01/29/scala-type-level-enc...
* Java: https://arxiv.org/abs/1605.05274
jules
Dependent types go beyond what Zig does by removing the distinction between comptime variables and runtime variables (so types can depend on runtime variables). Zig goes beyond dependent types in the sense that the comptime/runtime distinction allows Zig to handle all comptime values at compile time, which is important for efficiency. It would be interesting to combine the two approaches, via partial evaluation or staging.
dnautics
> the type checker can loop forever
Oversimplifying: Zig anyways gives you "compiler branching tokens" so your type system can't loop forever, if I'm not mistaken
specialist
Do you have a preferred (favorite) implementation of dependent types?
Quickly scanning the list on wiki... Ada is the only imperative language listed.
Corecursive has had a few episodes about dependent types. Here's two:
https://corecursive.com/015-dependant-types-in-haskell-with-...
https://corecursive.com/023-little-typer-and-pie-language/
Learning more is on my to do list. To noob me, the descriptions remind me of Eiffel's constraints (asserts, pre/post-conditions). And maybe user defined typedefs, like constraining dayOfWeek to values 0..6, a bit like enums.
skybrian
It looks very nice and I look forward to using it.
A downside for package authors (once Zig gets to the point of having a package ecosystem) will be that syntax errors are checked only with the comptime parameters that are actually used at call sites. To maintain compatibility, authors of public APIs will need tests with commonly-used comptime parameters.
It seems like a good way to avoid lots of type-level complexity in the language, though.
jstimpfle
I've grown used to the idea that generics (data structure macros, C++ templates...) aren't that useful. If I find myself in a situation where I'm thinking of a solution that involves generics, I stop and ponder what is actually the essence of the repeated stuff. It rarely is on the syntactic level, often it runs deeper. Probably the commonalities can be distilled into a data structure.
Simple example: Intrusive linking headers (e.g. Linux kernel list.h). While those can benefit from an optional very very thin generics layer on top, essentially the code for linking e.g. nodes in a tree should be the same regardless of the data structure where the headers are embedded.
Getting this right simplifies the code but can also speed up compile times.
pcwalton
How do you write a generic quicksort function without generics?
There are two ways I know of: (1) throw type and memory safety out the window and use void pointers plus size/alignment like qsort(3) does; (2) require that users manually write an interface with a swap(int i, int j) function like Go before generics does. Both solutions are really bad.
joe_guy
Void pointers also add an unfortunate layer of dereferencing.
In C# for example, generics are important to keep values on the stack instead of the heap, not only avoiding garbage collection but improving data locality.
jstimpfle
I rarely ever do need a sorting algorithm for what I do. I might not have done even one sort call in 2021. Usually the data I have is sorted by construction, or doesn't need to be sorted in any particular order.
When I do need to sort, I just use qsort from libc. It's also easy to write my own version of qsort.
I once measured qsort vs std::sort on plain ints (basically the most pessimistic case for qsort vs std::sort) and the difference was like 2x. If this becomes a bottleneck then it's worth investigating how to take advantage of additional context to speed up the sorting much more than any generic sorting algorithm could do anyway. Simple example: bucket sort. (I'm 100% confident sorting performance hasn't ever been noticeable in my career so far, but I've done simple optimizations like that once in a while for fun).
> Both solutions are really bad.
Due to what I described above, I'm actually in favour of qsort - no new code is generated, just a simple library call that takes a pointer to the comparison function. Really really easy to use.
undefined
mpweiher
Use Smalltalk?
ncmncm
Generics are the way that useful semantics can find their way into useful libraries. A collection of useful libraries is what is needed to make a language useful.
This is why C++ usage is still growing fast: almost every new feature makes it possible to write more powerful libraries that get, thereby, easier to use.
dataangel
> This is why C++ usage is still growing fast: almost every new feature makes it possible to write more powerful libraries that get, thereby, easier to use.
LOL no, it's growing because people can't get the performance and battery life they want out of other languages and it's a hugely entrenched player with a gigantic existing ecosystem and is able to seamlessly call into all existing C code as well. Language features after C++11 (arguably C++98) have very little to do with new growth except keeping interest by showing it's alive and spurring book sales and conference tickets. Most libraries still target old versions of the standard and don't use most new features. C++14 vs C++17 vs C++20 makes very little difference for 99% of users. Most people are not using polymorphic allocators to deserialize Unicode inside their constexpr DSL parser.
jstimpfle
This is a blanket assertion. Why should useful semantics require generics? Why can't they come with simple fixed data structures? If you can provide a nice counterexample to the example that I gave, this would be more convincing.
Absent the absolute necessity of generics to support essential semantics, I'm probably in favour of a simpler generics-less version. I don't see why "more powerful libraries" get automatically easier to use. It could often be the opposite.
There is a tradition in some C++ subcultures where they try to cram in as many invariants as possible into types, concepts, templates, etc. at all costs. I tend to think that if all this heavy machinery is needed, the functionality might be too complicated from the start.
tsimionescu
> the code for linking e.g. nodes in a tree should be the same regardless of the data structure where the headers are embedded.
Why should that be so? It's particularly NOT true for one of the simplest and most efficient data structures in any program: the array. The code that needs to work with an array needs to know exactly the size of each element of the array, so you basically can't have an efficient language that doesn't support generic arrays. Haskell maybe sometimes gets by with a very smart compiler and heavy use of laziness, but even C has support for generic arrays.
jstimpfle
You can totally make array iteration generic, just pass the element size. Not saying it is necessarily a good idea.
I agree that there are cases where the "there should be a single central piece of code" argument is not a strong one, because the generic code is so simple that it should be inlined into the usage code. Arrays iteration is one example - but note that for example sorting code is, while the sorting algorithm might be generic, more than just generic array iteration.
Doubly linked lists already profit from shared code in some ways, and definitely I'd find it a bad idea to replicate for example tree mutation code (RB tree or similar) for each node type. Embedding the header structure and keeping the links just between these embedded headers, to be able to reuse a common (compiled) piece of code is a very good idea here IMO.
undefined
AnIdiotOnTheNet
> Initializing arrays is weird in Zig. Lets say you want to have a 0 initialized array, you declare it like [_]u8{0} * 4 which means I want an array, of type u8, that is initialized to 0 and is 4 elements long. You get used to the syntax, but it’s not intuitive.
Alternatively:
var some_array = std.mem.zeroes([4]u8);
Though as mentioned later in the article the standard library documentation is not very good, making this not as obvious as it could be.> Everything in Zig is const x = blah;, so why are functions not const bar = function() {};?
Good question, there's an accepted proposal to fix this: https://github.com/ziglang/zig/issues/1717
> The builtin compiler macros (that start with @) are a bit confusing. Some of them have a leading uppercase, others a lowercase, and I never did work out any pattern to them.
In idiomatic zig, anything that returns a type is uppercased as though it were itself a type. Since only a few builtins return types, the vast majority of builtins will start with a lowercase letter. I think it is only `@Type`, `@TypeOf`, `@This`, and `@Frame` that don't.
rgrmrts
Any recommendations on learning more about what constitutes idiomatic zig? This is an issue I have with learning any new language - it’s kind of hard for me to figure out what writing idiomatic code in that language looks like. I usually go looking for popular/high quality projects and reading that code but it takes away from the experience of actually just toying around not to mention it being hard knowing what a high quality project is. Thanks in advance!
ithkuil
I don't know zig. Is one a constant initializer while the other is not?
anonymoushn
It looks like the value returned by std.mem.zeroes will end up being a compile-time constant, but I'm only like 90% on this.
Shadonototra
if you need to import a package to clear an array, something went very wrong somewhere..
kristoff_it
In Zig zero initialization is not idiomatic. Unless you have an active reason to do so (and during AoC you need zero init a lot more than normal IME), you should just set the array to undefined like so:
var foo: [64]usize = undefined;christophilus
Why?
Shadonototra
because it's trivial, it's like assigning a value to an integer, it shouldn't require a package
anatoly
I also did AoC 2021 in Zig: https://github.com/avorobey/adventofcode-2021
One thing the OP didn't mention that I really liked was runtime checks on array/slice access and integer under/overflow. Because dealing with heap allocation is a bit of a hassle, I was incentivized to use static buffers a lot. I quickly figured out that I didn't have to worry about their sizes much, because if they're overrun by the unexpectedly large input or other behavior in my algorithms, I get a nice runtime error with the right line indicated, rather than corrupt memory or a crash. Same thing about choosing which integer type to use: it's not a problem if I made the wrong choice, I'll get a nice error message and fix easily. This made for a lot of peace of mind during coding. Obviously in a real production system I'd be more careful and use dynamic sizes appropriately, but for one-off programs like these it was excellent.
Overall, I really enjoyed using Zig while starting out at AoC problem 1 with zero knowledge of the language. To my mind, it's "C with as much convenience as could be wrung out of it w/o betraying the low-level core behavior". That is, no code execution hidden behind constructors or overloads, no garbage collection, straight imperative code, but with so much done right (type system, generics, errors, optionals, slices) that it feels much more pleasant and uncomparably safer than C.
(you can still get a segmentation fault, and I did a few times - by erroneously holding on to pointers inside a container while it resized. Still, uncomparably safer)
pcwalton
> (you can still get a segmentation fault, and I did a few times - by erroneously holding on to pointers inside a container while it resized. Still, uncomparably safer)
This is a severe problem, and I predict that this is going to cause real security issues that will hurt real people if Zig gets used in production before it gets production-ready memory safety. This exact pattern (pointers into a container that resized, invalidating those pointers) has caused zero-days exploited in the wild in browsers.
elcritch
> This is a severe problem, and I predict that this is going to cause real security issues
That is a nasty problem, particularly in larger projects with different subsystems interacting (like say an xml parser and another).
I suspect it's worse in some ways as Zig has good marketing as being "safer" language despite still having the same fundamental memory flaws as C/C++. In the worse case that could lull programmers into complacency. I mean it looks "modern" so it's safe right? Just do some testing and it's all good.
Currently I'm skeptical Zig will get a production-ready memory safety. Currently there's only GC's or linear/affine types and Zig doesn't appear to be pursuing either. Aliased pointers aren't something that's properly handled by adhoc testing IMHO.
Laremere
FWIW, "safe" doesn't appear anywhere on the Zig homepage. I've been trying out Zig for the past couple weeks, and while I love it so far, it gives anything but the feeling of safety. I would say there's guardrails, but those are optionally disabled in the compiler for faster execution.
It seems to be that Zig is really not trying to be a replacement for all programming, but fill its niche as best it can. If your niche requires memory safety as a top priority because it accepts untrusted input, Rust would probably be a better choice than Zig.
pcwalton
Some sort of pointer tagging system, like 128-bit pointers where the upper word is a unique generation ID, might be the simplest approach to eliminate security problems from use-after-free, but it's going to have some amount of runtime overhead (though new hardware features may help to reduce it).
Alternately, use a GC.
dnautics
If you write tests in zig you will probably find this using the testing allocator. Yes, I get that some people really don't like writing tests.
pcwalton
Many of the highest-profile memory safety security issues are in very well-tested codebases, like browsers.
formerly_proven
Yes, when I invariably had to debug the first UAF in Zig I did pause for a bit and pondered my rust. It's definitely an argument against Zig that is unlikely to go away anytime soon.
gameswithgo
Zig is not memory safe on purpose. So when you need or want that you don’t use Zig
skybrian
Zig apparently has valgrind support. Maybe it’s not turned on by default?
AndyKelley
A better way to put it is "valgrind integration". It is enabled by default (when compiling for a target that has Valgrind support). Mainly it integrates with the `undefined` language feature which helps catch branching on undefined values. The nice thing you get beyond what C gives you, is that in Zig you can set things to undefined when you are done with them. Meanwhile in C, Valgrind is only aware of undefined for uninitialized variables.
But as others have pointed out, although Valgrind is a nice debugging tool, you would not run your application in it under normal circumstances. It's also not available on some important targets, such as macOS and Windows.
formerly_proven
I don't think Zig has any particular Valgrind support, it's just a binary after all. In order to properly utilize valgrind though you're going to have to change from the GPA or whatever allocator you're using to the libc one so that Valgrind can trace memory allocations correctly via preloading.
staticassertion
Valgrind support is cool but it's not a solution to the problem.
geokon
"runtime checks on array/slice access and integer under/overflow"
I'm probably missing something. I feel like you'd get this and a lot of the other benefits you list if you just compile C/C++ with Debug options - or run with Valgrind or something. Are you saying you get automatic checks that can't be disabled in Zig? (that doesn't sound like a good thing.. hence I feel I'm missing something :) )
pcwalton
You're correct: you do get virtually all of the safety benefits of Zig by using sanitizers in C++. (Not speaking to language features in general, obviously.) In fact, C++ with sanitizers gives you more safety, because ASan/TSan/MSan have a lot of features for detecting UB.
Especially note HWASan, which is a version of ASan that is designed to run in production: https://source.android.com/devices/tech/debug/hwasan
AnIdiotOnTheNet
The runtime safety checks are enabled in Debug and ReleaseSafe modes, but disabled in ReleaseFast and ReleaseSmall modes. They can be enabled (or disabled) on a per-scope basis using the `@setRuntimeSafety` builtin.
tialaramex
What "Debug options" are you imagining will provide runtime checks for overflow and underflow in C and C++ - languages where this behaviour is deliberately allowed as an optimisation?
In C it's simply a fact that incrementing the unsigned 8-bit integer 255 gets you 0 even though this defies what your arithmetic teacher taught you about the number line it's just how C works, so a "Debug Option" that says no, now that's an error isn't so much a "Debug Option" as a different programming language.
saagarjha
> What "Debug options" are you imagining will provide runtime checks for overflow and underflow in C and C++ - languages where this behaviour is deliberately allowed as an optimisation?
-fsanitize=undefined.
> In C it's simply a fact that incrementing the unsigned 8-bit integer 255 gets you 0 even though this defies what your arithmetic teacher taught you about the number line it's just how C works, so a "Debug Option" that says no, now that's an error isn't so much a "Debug Option" as a different programming language.
Yes, but this happens to be defined behavior, even if it’s what you don’t want most of the time. (Amusingly, a lot of so-called “safe” languages adopt this behavior in their release builds, and sometimes even their debug builds. You’re not getting direct memory corruption out of it, sure, but it’s a great way to create bugs.)
vinkelhake
Runtime checks for signed overflow can be enabled with -ftrapv in GCC and clang. Having this option open is why some people prefer to use signed integers over unsigned.
not2b
C unsigned integers are completely well behaved: they do arithmetic modulo 2^n, and I hope you had a teacher that exposed you to that. C has many problems but that isn't one of them: overflow of unsigned is designed and documented to wrap around.
bruce343434
-fsanitize=address,undefined,etc
There's even threadsanitizer which will tell you about deadlocks and unjoined threads.
typon
Defaults matter a lot. Just because something is possible doesnt mean it is likely to happen.
Are most people going to enable asan, run their programs through valgrind extensively, or just do the easy thing and not do any of that?
This is also why neovim is being actively developed and successful and vim is slowly decaying. The path of least resistance is the path most well travelled.
snovv_crash
Any project with a decent test coverage and CI can easily set up an ASAN / Valgrind run for their tests. I know I've had this on the last few C++ codebases I've worked with.
superjan
I would say that keeping the checks in runtime for release builds is the smart default. For most usages, removing the checks in release builds only adds security holes without measurable impact on performance.
djur
Slices allow catching a lot of bounds errors that you can't reliably catch when using raw pointers.
rslabbert
For what it's worth, I find a lot of Zig code benefits from switching to u32/u64 indexes into an array instead of using pointers. This is only really doable if your container doesn't delete entries (you can tombstone them), but the immediate benefit is you don't have pointers which eliminates the use after free errors you mentioned.
The other benefit is that you can start to use your ID across multiple containers to represent an entity that has data stored in multiple places.
See [1] for a semi-popular blog post on this and [2] for a talk by Andrew Kelley (Zig creator) on how he's rebuilding the Zig compiler and it uses this technique.
[1] https://floooh.github.io/2018/06/17/handles-vs-pointers.html [2] https://media.handmade-seattle.com/practical-data-oriented-d...
dnautics
> Everything in Zig is const x = blah;, so why are functions not const bar = function() {};?
This may or may not happen: https://github.com/ziglang/zig/issues/8383
> Fixing the standard library documentation would be my biggest priority if I worked on Zig, because I think that is the only thing holding back general usage of the toolchain.
This is a valid concern, but I believe the zig team is deliberately holding off on improving the std lib documentation, because they are expecting (potentially huge, maybe not? who knows) breaking changes down the line. The "stdlib is not documented" is a deliberate choice to signal "use at your own risk, especially with respect to forwards compatibility".
> there are still quite a few bits of syntatic sugar hiding the real cost of certain operations (like the try error handling, there is implicit branches everywhere when you use that...
I dunno, that's like saying that `if` hides branching. It's a language-level reserved word, you're expected to understand how they work under the hood.
ArtixFox
yes, our first priority is stage2, after that, we might deal with stdlib. Andrew is going to go through the stdlib before the 1.0 release.
dnautics
it's super reasonable to expect language-level stability before shoring up the stdlib. I know 'gatekeeping' is a bad word sometimes here, but this is soft-gatekeeping, and imo, a good thing (for now) to help focus the language.
ArtixFox
unfortunately yes, it somewhat is, but the devs try to maintain extremely readable source, not the best thing but i think its really good and important cuz its the best example of good zig code and might teach you a bit or two like i learnt how to write saner and better code.
and the stdlib breaks sometimes soo its better to not put a loot of effort in docs
dureuill
> Try and read a moderately complex Rust crate and it can be mind boggling to work out what is going on.
I do that all the time, even reading the source of the std, something that I cannot do sanely in C++. IME Rust code is easy to read, with symbols that are always either defined in the current file, imported, or referred to by their full path.
dagmx
Agreed. This was my first AOC, and I did every day in rust (except for one that I did by hand).
Multiple times I'd go look at the source of a data structure and it reads very easily. I'd even share my code with friends and coworkers who weren't familiar with Rust (so we could compare..they were most familiar with Python). Not only could they easily grok my code, I showed them how docs.rs let's you easily see source. All of those that looked, could read it easily with some explanation from me on traits, pattern matching and generics.
I think it's obviously a subjective thing...but I very much disagree with the author that idiomatic Rust is difficult to read or comprehend.
In fact, I find Rust easier to grok, because I need to keep less in my head at any given time. Function bodies become almost self contained, without me having to think about lots of details like errors and return validity etc...
tialaramex
To be fair, the thing that makes a working C++ standard library unreadable is also a hazard in understanding Rust's std. Macros. The macros in a C++ standard library are horrible, because it is here that essential compliance and compatibility are squirreled away, and because the C++ macros aren't hygienic they're bigger than they'd otherwise need to be (e.g. you mustn't call it foo, say __x5_foo instead). But while they're far more readable on their own terms, the Rust macros littering std do mean it's harder to see how say, a trivial arithmetic Trait is implemented on u32 because a macro is implementing that trait for all integer types.
A macro-pre-processed std might be easier for the non-expert rustacean to grok even though it isn't the canonical source.
The symbol thing is pure insanity, machines have no problem knowing what symbol8164293 refers to, but humans can't get that right, and programming languages, including in theory C++ are intended for humans to write.
khuey
The thing that makes the C++ standard library source difficult to understand in my experience is heavy usage of templates and very deep inheritance chains.
pcwalton
The _Weird_identifier_naming_convention that the STL has to use to avoid colliding with potential user-defined macros doesn't help either.
afdbcreid
Remember that expanding macros includes things like `println!()`. I'm not sure beginners will find the following particularly easy to read:
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["Hey ", "!\n"],
&match (&name,) {
_args => [::core::fmt::ArgumentV1::new(
_args.0,
::core::fmt::Display::fmt,
)],
},
));
};
Although, to be honest, I don't think there are many usages of these macros in std.tialaramex
Good point. I hadn't considered panic!() in particular which is used in std, and the Debug implementations in std won't make a huge amount of extra sense after macro-pre-processing either.
skrtskrt
Macros are extremely hard to grok and and so many use such short variable names that it looks like absolute gibberish.
They also look so different than normal Rust code. Python metaprogramming still looks exactly like Python, for example.
afdbcreid
Macro definitions can be hard to grok, but that's not usually what you look on.
Macro uses can be hard, but macros are not used commonly in Rust (I mean, there are not many macros - but those that exist are very common). And they also look very much like Rust: `vec![a, b, c]` vs. `[a, b, c]`, `zip!(a, b, c)` (itertools) vs. `zip(a, b)` (`std::iter::zip()`), `#[tokio::main] async fn main() {}` vs `fn main() {}`...
Attribute procedural macros only accept valid Rust syntax, and most of them are derive macros that just derive some trait.
ReactiveJelly
Oh yeah. I don't know if I've ever tried to implement a macro. The macro_rules syntax is hard to read and it doesn't feel like there are many examples explaining how it works.
afdbcreid
The Little Book of Rust Macros - https://veykril.github.io/tlborm/introduction.html.
nu11ptr
Agreed, when I see comments like this I tend to think they haven't spent much time using the language. It takes a while, but after a month or so you can read just about any Rust code. Honestly, feels like a much simpler language in day to day usage than say a language like Scala (just an example) to me.
oxymoron
I also agree with this sentiment, although there are some examples of really weird meta programming that remains opaque to me. For instance, I’m able to use `warp` as a framework, but the use of things like type level peano arithmetic is mostly incomprehensible to me at the moment. I also find that I run into Higher Rank Trait Bounds so rarely that I have a poor grasp of it (which might be as intended). All that to say that there are some odd corners of the language, given that I’ve been using it for five years now and as my main professional language for three years.
root_axis
I love Rust, but e.g. macros, lifetimes, generic trait parameters etc are all very difficult to parse for the uninitiated. Of course, I'd bet on the readability of rust over cpp template wizardry any day of the week.
flohofwoe
The part about making things easy to type is interesting, because this generally only works with a single international keyboard layout (usually US English), e.g. making things easy to type on the US keyboard layout may make it harder on an international layout.
It's an old problem though, for instance the {}[] keys are terribly placed on the German keyboard layout, requiring the right-Alt-key which was enough for me to learn and switch to the US keyboard layout, and not just for coding.
I think a better approach for a programming language would be to use as few special characters as possible.
PS: Zig balances the '|' problem by using 'or' instead of '||' ;)
e12e
It's a bit bizarre to complain about the pipe symbol IMNHO (as a user of Norwegian kbd layout, where åæø/ÆØÅ takes up prime estate) - without pipe you can't use a posix shell at all - so if you're on a layout without pipe, it's not like you likely could use any languages outside Smalltalk/Self, or possibly Pascal...
That said, yes, I think there's room for languages with very limited use of special characters. But I think they'd always be somewhat specialized.
Like Markdown.
formerly_proven
I've heard this before but personally I've never had a problem with {}[], I just use the right thumb for shifting to the ancient greek layer.
dralley
>PS: Zig balances the '|' problem by using 'or' instead of '||' ;)
I wish Rust had made that decision as well.
typon
While the standard library documentation is non existent, using grep on it and just reading through it is very easy, compared to almost any other language I have used.
I would actually say this is preferred: it's early days, so the documentation can't go out of sync because it doesn't exist, and library maintainers are incentivized to write understandable code, which most people who are getting into the language are forced to read, creating a consensus of what is considered idiomatic in the community.
kristoff_it
Yep, and we also encourage this, if you open the (incomplete, buggy) autogenerated doc for the standard library, you get a banner at the top that links you to a wiki page that explains how the standard library is structured.
https://github.com/ziglang/zig/wiki/How-to-read-the-standard...
zppln
I've also found the tests for the standard library pretty useful when digging around trying to figure out how to use stuff.
e12e
> For loops are a bit strange too - you write
for (items) |item| {}
>, which means you specify the container before the per-element variable. Mentally I think of for as for something in many_things {} and so in Zig I constantly had to write it wrong and then rewrite.That does feel like the syntax is missing an "each" or a "with", as in "for each somethings as some do" or "with each somethings as some" - or in a similar terse/compact syntax:
each (items) |item| {}
I'm surprised there's no mention about (lack of) string type - considering the domain (advent of code). I've not found the time to actually work on aoc this year, but I also had a brief look at starting with Zig - and quickly met a bit of a wall between the terse documententation on allocator, and the apparent lack of standard library support for working with strings.I think the documentation will improve as the language stabilizes and there's likely to be more tutorials that work with text (both trivial like sirt/cat/tac in zig, and more useful like http or dns client and servers etc).
llimllib
I found the standard library's support for strings was plenty fine, doing AoC problems in zig tests it out thoroughly. Tokenize[1], split[2] and trim[3] were the most common ones I used.
Was there something in particular you were looking for and didn't find?
[1]: https://github.com/ziglang/zig/blob/master/lib/std/mem.zig#L...
[2]: https://github.com/ziglang/zig/blob/master/lib/std/mem.zig#L...
[3]: https://github.com/ziglang/zig/blob/master/lib/std/mem.zig#L...
* After I read my own comment, I'd note that AoC tests out string manipulation pretty thoroughly but things like unicode handling not at all, so []const u8 as a string may be more annoying in the real world than in AoC answers and I haven't used zig's unicode facilities at all
tialaramex
In AoC it's completely fine to conflate a character and a byte. Neither your daily input nor the provided tests will have anything beyond ASCII.
Which is fine for AoC, good choice, but it means the language needn't get this right, or even provide any help to programmers who need to get it right, in a similar way to how "big" numeric answers in AoC will fit in a 64-bit signed integer, never testing whether your chosen language can do better if the need arises.
shakow
> Was there something in particular you were looking for and didn't find?
Unicode handling. Treating a string as a byte array is all fine and dandy if you're only processing english latin alphabet data, but it's a PITA as soon as you start using e.g. extended characters (math symbols, fancy quotes, ...) other languages, or emojis.
llimllib
There is the `std.unicode` module[1] which provides standard unicode functions (encode, decode, length, iteration over code points), so I don't think it's fair to say that the language's library lacks strings in any real sense.
I will re-emphasize that I've not used it, so I cannot speak for its quality.
[1]: https://github.com/ziglang/zig/blob/master/lib/std/unicode.z...
kristoff_it
Also here is a blog post that gives more info on how to deal with unicode in Zig.
chubot
What do you need to do with them? All my data is UTF-8, and low level code is generally parsing, which doesn't involve any of those characters. It generally just works with all special characters (e.g. on my blog).
I think Unicode on the server or CLI is very different than Unicode on the desktop/GUIs.
Since Zig interfaces well with C, it should be set up well for the harder desktop case, because the "real" Unicode libraries handling all the corner cases are written in C (or have C interfaces). I don't think even Python's unicode support is up to the task for desktop apps.
e12e
> Was there something in particular you were looking for and didn't find?
Mostly dealing with allocation - ie dynamic string handling, reading strings from a file, passing a string to a function, and returning a different string to another function and so on.
Ed: this was pretty much before solving concrete aoc problems, just figuring out how to read "suffix" from standard input, passing it to a function "prefix_suffix", getting a string "prefix suffix" back and outputting that string; trivial manipulations, but lots of dynamic allocation.
chubot
I thought this syntax was weird too, but one cool thing is that you can ask for the index too
for (items) |i, item| {
}
I guess Go does something similar, but it's a little weirder IMO because it has maps, while Zig doesn't.I suppose it could have been
for i, item in items {
}
But I guess coming from Python that feels like tuple unpacking, which I don't think Zig has, but could make sense?ReactiveJelly
Lua also has
for i, v in ipairs (t) do
-- i is the index, v is the value
end
Not getting the index in C++ easily was very frustrating. Often in C++ I fall back on C-style for-loops just because the functional stuff is hard to remember.In Rust it doesn't give you the index by default, but the `enumerate` adapter adds an index anywhere in a functional pipeline.
kzrdude
In "The Good" section, the author says there are only while loops and no for.. but apparently there is a for, now I'm unsure what it means. Is `for` a function?
eirojhupp
for strings, these might be good (haven't tried them yet): https://github.com/jecolon/zigstr https://github.com/jecolon/ziglyph
winter_squirrel
It saddens me that a proper string type is a hill the zig folks are willing to die on. The language would have _felt_ a lot better if they would have (at least) just copied what rust did with strings.
I understand the argument that in most situations a byte array might be what you actually want, but in practice it feels very dirty to be passing byte arrays around instead of expressing the underlying meaning of that byte array as a type (in this instance a String type).
Having a string type also makes any standard library functions on strings infinitely easier to discover.
gilbetron
> One nugget of knowledge I’ve worked out though - Zig is not a replacement for C. It is another replacement for C++.
I hope this isn't the case, since I see Rust as the C++ replacement, and another replacement isn't very interesting to me. The main reason I've been interested in Zig is because I thought it was a replacement for C, which is an interesting idea.
dnautics
I don't understand where that came from. It's really a replacement for C. The place where complexity comes from in zig is pretty much the comptime type system, which is emergent from the idea of replacing irregular consteval rules for C and replacing preprocessor macros
I would say that Zig is:
C - {make, autoconf, etc., preprocessor, UB[0]} + {*defer, !, ?, catch/try, "async"[1], alignment, comptime}
I don't think that rises to the level of "C++ replacement". Maybe it's that comptime lets you do generics a la C++ templates?
[0] by default, in zig you can have UB for performance
[1] in quotes because async is not actually async, it's a control flow statement that is usually and most usefully used to do async things.
ncmncm
Hint: Rust will not be replacing C++. C++ and Rust will coexist indefinitely. At some point in the future, it is possible that more Rust coders will be using it daily in their work than the number who pick up C++ for the first time in any given week, who will go on to use it professionally. Or, that might not happen, and Rust will join Ada and so many other languages that never got their miracle.
Even if Zig doesn't fizzle like the overwhelming majority of languages, it won't replace, or displace, C, never mind C++. Everybody willing to move on from C already did a long time ago. People still using C today like it for its failings, so a language that fixes them is exactly what they don't want. It doesn't give C++ users any of the things they need.
The only real advance in systems languages in the last 50 years is the destructor, so it is frankly weird to find a new language without it. The Drop trait is all that makes Rust a viable prospect for its own miracle.
gilbetron
Oh I definitely meant "replacement" as in "replacement for me and many people", not that C++ would vanish. C and C++ are not going anywhere.
oconnor663
Is a C replacement (which is not also a C++ replacement) really what anybody wants? Like with no generics, no dedicated error handling, and no automatic cleanup? I get that everyone enjoys a simple language, but these features feel like table stakes now.
Bayart
Exception handling and garbage collection are two features that feel superfluous, if not outright noxious, to a number of programmers; in particular those doing systems, embedded, real time programming. That is the crowd that still uses C. There's a cost associated with the execution environment taking control over from the program itself, and by proxy the programmer. It's not something you want when you design your code with the assumption that is it a more or less accurate representation of its execution flow.
Long story short : those who want that sort of programming language know why.
oconnor663
Agreed about GC and exceptions, but I wanted to focus on other approaches. Rust and Go are good examples of doing error handling through regular return values. Similarly Rust and C++ do cleanup with destructors, and Go does at least some of it with deferred function calls. There are ways to do these things that are suitable for low-level programming. But doing nothing no longer seems viable to me.
To the extent that Zig's features in these areas (and also generics) make it "not a real C replacement", that's when I question whether a real C replacement is actually what anybody wants.
sirwhinesalot
Zig has all of those things, if you consider defer to be a form of automatic cleanup.
afdbcreid
Which it is not. It is harder to forget it, but it's possible.
isaiahg
That's the one part in which I really disagreed and the author does a bad job of explaining why they think that.
formerly_proven
The optionals story in Zig seems a bit weak to me, because it has dedicated syntax to support conditional unwrapping:
if(optional) |captured_optional| { ... }
if actually is three different syntaxes: if(expression) {} else {}
if(optional) |captured| {)
if(optional) |captured| {) else {}
if(errunion) |result| {} else |err| {}
The latter is kinda awkward because it looks exactly like the optional syntax until the else and you have to know the type of the variable to know which is which. Capturing doesn't allow shadowing, which makes the optional case awkward.This is one area that e.g. Kotlin has done better by checking if the expression of any if statement implies non-nullity of variables and then implicitly unwrapping them, as they can't ever be null:
if(optional != null) { use optional directly }
This works much better for multiple optionals: if(optA != null && optB != null) { can use both optA and optB directly }
You can write this in Zig as well, but it results in a sea of unchecked .?, while Kotlin while give you compile errors if you use an optional without unwrapping that was not implied to be non-null.Or you go multiple levels deep, as the if-optional syntax only allows one optional:
if(optA) |capturedOptA| {
if(optB) |capturedOptB| {
}
}
The error union story is fairly sound so far but one major annoyance is that while it composes well for returning errors, it doesn't compose well for error handling. You can't do: someComplexThing(file1, file2) catch |err| switch(err) {
CryptoErrorSet => handle_crypto_error(err);
FileErrorSet => ...
}
as switch does not support error sets for branches, only error values. This seems to me like it incentivizes you to do have either something like this: someComplexThing(file1, file2) catch |err| {
if(cryptoErrorToString(err)) |errdescription| {
// ...
}
if(ioErrorToString(err)) |errdescription| {
// ...
}
}
Or just a huge handleAllTheErrorsPls thing.Errors are also just a value - if you want some extra information/diagnostics to go along with an error, you'll have to handle that yourself out-of-band.
On errors, Zig doesn't seem to have a strerror for std.os errors - awkward.
kristoff_it
> you have to know the type of the variable to know which is which.
That's not correct. The error version differs from optional by the capture on the else branch. The optional version can't have it, and all error versions must have both captures. You can always tell which case it is just by looking at the code, without having to know the types involved.
formerly_proven
Yes, I meant if you're looking at the if() part - you either have to know if that's an optional or an error, or go looking for the else to see if that captures an error.
> because it looks exactly like the optional syntax until the else
jmull
Consider the difference between an optional and an error union:
An error union is a value or error.
An optional is a value or null.
The if/else capture syntax follows: "if" captures the value. "else" captures the error if there can be one, or doesn't capture anything if there can't. That is, you don't need (and therefore don't want) a difference between error union and optional in the if part of the syntax.
dnautics
write shorter blocks so you can see the else? If the ifs get too nested, encapsulate them in functions? Buy an extra monitor and mount it vertically (or diagonally)?
anonymoushn
> One nugget of knowledge I’ve worked out though - Zig is not a replacement for C. It is another replacement for C++.
While comptime is a potential source of complexity, I sort of think C++ developers won't accept a replacement that has no RAII or automatic invocation of destructors.
tomcam
Meh. Not trying to start a language war but I was grateful to switch from C++to Go when the price was to lose generics and a few other things in exchange for the language’s simplicity and clarity.
dnautics
I think there are so many corners where people are using C++ that making generalizations about them is likely to fail.
tomcam
That’s why I referred exclusively to my preferences. It is obviously the more comprehensive and versatile language.
undefined
Bekwnn
There's an open issue to add some kind of function annotation+errors for functions which require you to call a cleanup function.
The discussion has had a lot of back and forth and they haven't really settled on a desirable solution yet, but it's something they're hoping to add.
https://github.com/ziglang/zig/issues/782
I work in games with C++ and we already do so much manual management and initialization+teardown functions that lack of RAII isn't a deal-breaker. Though I'd definitely prefer it if there was something either well-enforced or automatic.
anonymoushn
This sounds good. While I don't have much preference about "being explicit" vs having automatically-invoked dtors, it will be nice to be nudged when I actually forget to clean up.
balaji1
I got into Rust by working on Advent of Code 2021. The problems seem arbitrary, repetitive and sometimes unnecessarily hard. But they are well-designed for starting on a new language. We are forced to repeatedly use basic concepts of a language, so that is useful to get a few reps in on a new language. We are also forced to build utils that can be used a few times.
And if you challenge yourself to solve the problem as quickly as possible so as to see where the story leads, you can stay motivated to work thru the problems. Helps if you have a friendly competition going with a few friends.
Get the top HN stories in your inbox every day.
Computer language inventors are torn between a voice whispering “use the language Luke” and and a more gravelly “let your feelings for the compiler grow, embrace the syntax side.”
I did a two-day sprint through Zig a month ago and really really liked it. It has some quirks that I would have done differently, but overall I think it’s pretty cool. I just need a good small scale project to try it out on now.
My favorite example of the “use the language” ethos is the way Zig does generics. I have hated generics/templates in every language I use them in. They are the gordian knot of confusion that most languages in pursuit of some sort of unified type theory impale themselves on (yes, I’m mashing metaphors here). But Zig just uses its own self to define user types and generics come along for free. It resonates (with me at least) as a realization of what Gilda Bracha has shared about how to reify generics.
[1] https://gbracha.blogspot.com/2018/10/reified-generics-search...