
Why Zig When There Is Already CPP, D, and Rust? (2017) - luu
https://github.com/ziglang/zig/wiki/Why-Zig-When-There-is-Already-CPP,-D,-and-Rust%3F
======
WalterBright
"C++, Rust, and D have a large number of features and it can be distracting
from the actual meaning of the application you are working on. One finds
themselves debugging their knowledge of the programming language instead of
debugging the application itself."

It's an interesting point. My experience with it, though, is that if the
language is too simple, the complexity gets pushed off into the application.
The result is "boilerplate", a lot of code that gets routinely cut&pasted into
applications.

The thing about complexity in the language, on the other hand, is once you do
learn the language, that skill follows you with every app you work with. With
complexity pushed to the app, you have to re-learn it with every app.

A screwdriver can do a lot of things - chisel, hammer, scraper, prybar, punch,
paint stirrer, shim, etc. But I prefer having a set of tools designed to be
chisels, hammers, etc. My projects come out a lot nicer.

~~~
nerdponx
_It 's an interesting point. My experience with it, though, is that if the
language is too simple, the complexity gets pushed off into the application.
The result is "boilerplate", a lot of code that gets routinely cut&pasted into
applications._

This is a frequent complaint I hear about Go: they made the language too
simple in certain places. People also tend to cite Rich Hickey's distinction
between "simple" and "easy".

So the question about Zig becomes one of what exactly was kept simple, and
what was allowed to become complex?

I think the world would benefit from a Hyperpolyglot-style comparison here.
Rosetta Code is too much of a mess; ideally, I want to see a thoughtful side
by side comparison of the same program written in several languages, with
commentary.

~~~
int_19h
Zig might get away with it by restricting itself to a narrow niche where this
kind of simplicity is advantageous. Go tries to eat a much larger piece of the
pie, and the deficiency is felt more as a result.

~~~
veebat
There's a profound wisdom to knowing the niche you're in, and Zig does seem to
have that. The applications part of the stack is saturated with options, all
trying to encroach on each other, but when it comes to the genuinely low-
level, unsexy stuff...the viable options are all pretty old languages, and
languages that patch over those old languages.

And with respect to discussion of boilerplate, you can, of course, always have
a "Zig++" language that patches over Zig. By cutting out its own
metaprogramming functionality, it's much more straightforward to generate
useful Zig source. There are a lot of advantages to restraint in the design,
and Go does share that, too.

------
jordigh
I'm just happy that D was mentioned. Yay, D! Also, D's metaprogramming is an
utter joy, almost lisp-like, without the craziness of C++ templates. Give it a
try!

An intro to one ingredient of the magic of D metaprogramming, compile-time
function evaluation:

[https://tour.dlang.org/tour/en/gems/compile-time-function-
ev...](https://tour.dlang.org/tour/en/gems/compile-time-function-evaluation-
ctfe)

An example of the kind of introspection available in the D stdlib:

[https://wiki.dlang.org/Finding_all_Functions_in_a_Module](https://wiki.dlang.org/Finding_all_Functions_in_a_Module)

This one's really cool, a library that lets you build an entire grammar of
your choice and evaluate it at compile time:

[https://github.com/PhilippeSigaud/Pegged](https://github.com/PhilippeSigaud/Pegged)

Nim's metaprogramming seems quite similar, and I sometimes wonder if perhaps
it took some inspiration from D.

~~~
542458
> compile-time function evaluation

Sorry, potentially stupid question incoming: I thought this was pretty
standard for c++ optimizing compilers. What makes D's version of this special?

~~~
VHRanger
It means you can run compile time programs without resorting to crazy template
metaprogramming

~~~
zamalek
C++ now has constexpr, so that pro is now shared by both C++ and D, somewhat
(ignoring templates, D is still more versatile).

That being said, C++ template metaprogramming can be pretty elegant[1] (there
was a video where she describes the template functional style, but it seems to
have been deleted).

[1]: [https://github.com/hanickadot/compile-time-regular-
expressio...](https://github.com/hanickadot/compile-time-regular-expressions)

~~~
WalterBright
constexpr accretes more power with every iteration, but it's still got a ways
to go:

[https://en.cppreference.com/w/cpp/language/constexpr](https://en.cppreference.com/w/cpp/language/constexpr)

[https://dlang.org/spec/function.html#interpretation](https://dlang.org/spec/function.html#interpretation)

------
comex
Eschewing all hidden function calls is a reasonable design choice, but you
give up a lot.

No destructors means no smart pointers – and Zig doesn't seem to have a
garbage collector, so I guess you need to free() things manually.

No operator overloading means, among other things, no custom numeric-like
types – including wrapper types for units (e.g. Feet) unless the language
provides separate support for those (e.g. Go does, Zig seemingly doesn't).

~~~
jetzzz
C++ and Rust need destructors because they both have unwinding and thus need
to know how to destroy things. If there is no unwinding then calling
destructors manually is not that bad if compiler checks that you do it. That
said, I haven't used zig and don't know how it actually works.

~~~
ioddly
I have yet to use Zig seriously, but I'm pretty sure "defer" is its solution
to this problem:
[https://ziglang.org/documentation/master/#defer](https://ziglang.org/documentation/master/#defer)

~~~
edflsafoiewq
I asked this in another thread, but how do generics work in the absence of
destructors? Let's separate the idea of a destructor from the idea that a
destructor is implicitly called at the end of a scope.

In C++, if there weren't automatic destructor calls, you could still clean
things up manually with a pseudo-destructor call, x.~T(), where T is the type
of x.

In C, there are no (user-defined) generics, so you always know the concrete
type of x, so you know whether it needs to have a destructor called and what
the name of that destructor is.

In Zig, how would generic code create an instance of a generic type T that may
or may not need to have a destructor called, ie. how do you know if you need
to use defer and what the deferred expression should be? How would tooling be
able to check that destructors were called if there is no special way of
marking that a destructor exists?

~~~
isaachier
Having used Zig a bit, and C++ for years, I personally believe that this sort
of scenario is pretty rare. You can always add a no-op dealloc method to your
type if you want a generic function to handle it correctly. Moreover, Zig has
awesome metaprogramming support so you can probably think of a way to check
the type parameter for a dealloc method and only generate the dealloc call for
those types.

~~~
edflsafoiewq
Pretty rare? What about ArrayList? I want ArrayList(T) to work whether T = i32
or T = ArrayList(i32).

> You can always add a no-op dealloc method to your type if you want a generic
> function to handle it correctly

You're looking at it from the perspective of the generic consumer, not the
generic author. The generic author generally does not have the ability to edit
the type. Plus there are many types that do not have methods at all (integers,
points, etc.).

> Moreover, Zig has awesome metaprogramming support so you can probably think
> of a way to check the type parameter for a dealloc method and only generate
> the dealloc call for those types.

Yes, you can detect and call a method called deinit that has the appropriate
signature using @reflect. You can even put this in a function and call it a
pseudo-destructor. But there's no guarantee that deinit has the actual
semantics of a destructor without some kind of language-level agreement
between class authors.

~~~
AndyKelley
> I want ArrayList(T) to work whether T = i32 or T = ArrayList(i32).

This will work fine. Do you have a more specific example?

~~~
edflsafoiewq
When I call deinit on an ArrayList(ArrayList(i32)) do the elements have deinit
called on them?

~~~
AndyKelley
How are you expecting to call deinit on an i32?

~~~
edflsafoiewq
Not sure what you mean. The elements of an ArrayList(ArrayList(i32)) are
ArrayList(i32)s.

To answer my question, no, they're not deinited. All deinit does is call
self.allocator.free on the slice of elements, and for many allocators that's a
nop. In fact none of ArrayLists methods take any kind of ownership of its
elements. If you shrink an ArrayList(ArrayList(i32)) by one you leak the last
ArrayList(i32). None of the methods call destructors on the elements because
there is no generic notion of a destructor, only particular ad-hoc ones like
deinit methods. ArrayList appears to solve the problem I mentioned above about
generics not knowing if a generic type needs to have a destructor called by
only supporting types that don't.

In C++, you'd write the function that clears a vector something like

    
    
        void clear() {
            for (auto p = ptr; p != ptr + len; ++p) {
                p->~T();
            }
            len = 0;
        }
    

For a vector<vector<int>> the syntax p->~T() calls the destructor on a
vector<int> element. While for an vector<int> the syntax p->~T() is a pseudo-
destructor call, ie. it does nothing. This makes the same generic code work
when the elements of a vector need to have a destructor called and when they
don't.

~~~
AnIdiotOnTheNet
> Not sure what you mean. The elements of an ArrayList(ArrayList(i32)) are
> ArrayList(i32)s.

I believe what he was wasking was "given an ArrayList(i32), how would you
expect it to call deinit on the member i32s?". The answer, of course, is that
you don't, which is also true of ArrayList(ArrayList(i32)). ArrayList
absolutely supports heap-allocated types, you just have to free them yourself
before calling deinit() on the ArrayList itself.

~~~
edflsafoiewq
Which only brings us again (putting aside what virtue recommends that design
over having destructors) to the same original question. If I have an
ArrayList(T) for a generic type T, how do I know if the elements need to be
freed before I deinit and how do I do that if they do?

~~~
AnIdiotOnTheNet
I'm having trouble imagining a scenario where you'd have code that was so
generic it could take an ArrayList of any arbitrary type and would also be
responsible for its destruction.

But if you did, you could use `@TypeInfo` to inspect the inner type for a
function named `deinit`, or some other criteria that made sense for this
determination.

~~~
edflsafoiewq
Any generic data type with a private ArrayList(T) member is an example. Unless
you also expect callers to manually run destructors for elements of a type's
private members, and their private members, etc. And it's not just about
destroying the ArrayList entirely. Any function which just removes elements
from an ArrayList needs to know how to destroy elements or else pass the buck
for half its purpose to the caller. When I call shrink on an
ArrayList(ArrayList(i32)) I'm supposed to preloop over the shrunken-over
elements and call deinit on them before I call shrink? When I call the
function that removes elements satisfying a predicate I'm supposed to preloop
over the elements and call deinit on the ones I'm about to remove? Obviously
not.

I already mentioned that hack. All you're doing there is introducing the ad-
hoc notion of a destructors as a method named deinit. Again, in order for that
to work in generic code that convention needs to be blessed by the language.

~~~
AnIdiotOnTheNet
> Again, in order for that to work in generic code that convention needs to be
> blessed by the language.

Technically it only needs to be blessed by the standard library in this case.

------
oconnor663
> There are some standard library features that provide and work with heap
> allocators, but those are optional standard library features, not built into
> the language itself.

Note that this is also true of Rust. Programs build using `no_std` and
`libcore` don't have an allocator. A surprising amount of functionality is
still available, like sorting and string splitting.

~~~
nicoburns
Yes, but zig takes it a step further by allowing you to pass a heap allocator
into the standard array/vector type.

There are discussions about implementing something similar in Rust, but it
doesn't exist yet...

~~~
frutiger
C++17 allows that too with all the containers in the std::pmr namespace.

------
lixtra
I still don't get why they provide a warn() to stderr but no print() to
stdout. [1]

[1] [https://ziglang.org/documentation/master/#Hello-
World](https://ziglang.org/documentation/master/#Hello-World)

~~~
xyproto
If you have a command line application, you probably either want to use a
library that provides colors and more features, or output to file (where
writing to stdout is more appropriate than "print").

When developing, "print" is more commonly used as an aid for the developer.
Most "print" messages should either be removed or outsourced to a library when
the program nears completion. "warn" fills a similar purpose as "print".

If you really want "print", a small function that writes to stdout is quick to
write.

~~~
Phylter
If I'm writing a command line application then, most of the time, I just want
to let the user know what's going on. It doesn't always have to be fancy and I
don't want to track down a third party library to tell the user all went well
and give basic statistics. I'd go ballistic if I had to download a library to
print out "35 records processed." It just doesn't make sense.

~~~
AndyKelley
Then use std.debug.warn?

Quoting the docs linked above:

> Usually you don't want to write to stdout. You want to write to stderr. And
> you don't care if it fails. It's more like a warning message that you want
> to emit. For that you can use a simpler API:
    
    
        const warn = @import("std").debug.warn;
        
        pub fn main() void {
            warn("Hello, world!\n");
        }

~~~
tbodt
The message "35 records processed" isn't a warning, nor should it necessarily
go to stderr.

------
wycy
From what I've seen so far, I really like the clean syntax of Zig. I want to
like Rust, but it looks so verbose and loaded with special characters.

~~~
mikekchar
I'm curious. What do you (or other people) dislike about special characters.
For me, they are _more_ readable than text (maybe I should start writing APL
;-) ).

Granted, I'm dyslexic and apparently dyslexia only affects characters which
are phonetic. This has also been my experience and I _love_ Chinese characters
in Japanese text because it's _much_ more comfortable for me to read. So
that's why I might prefer symbols.

I'm wondering, though, why people dislike symbols (and I know it's a very
common thing). It's just like any other letter, except that it has a meaning.
I wonder, it is because it's hard to read phonetically (just the opposite of
my problem)? Whenever I've asked people they say "Symbols are ugly", which is
kind of an unhelpful response. It's like saying, "I don't like it because it
is unlikable". I hope someone has a slightly deeper insight into the reasons.

~~~
webmaven
The more symbols used, the less it looks like a human-consumable language
(note: "looks like", not "is").

The more symbols, the harder it is to unpack the code into (and restate in a)
human language. Even in your head. IOW, if you're mostly monoligual, and you
think about code in English (or whatever your preferred language is), more
symbols can actually make it harder to think about the code.

Trivial example: _a = a+b_ is slightly easier to say (and think) in English
than _a += b_

~~~
ealhad
> The more symbols used, the less it looks like a human-consumable language

I read this as “the more symbols used, the more it looks like mathematics”. I
guess this is really a matter of personal taste :)

------
hartror
> C++, Rust, and D have a large number of features and it can be distracting
> from the actual meaning of the application you are working on.

I've just started working in Rust and found it to have the right number of
features compared to C++.

------
lixtra
The article states

> Zig has no macros and no metaprogramming

but then in another place about zig[1]:

> Compile-time reflection and compile-time code execution

This does sound arbitrary more complex than macros (and is what actually got
me interested in zig).

[1] [https://www.patreon.com/andrewrk](https://www.patreon.com/andrewrk)

~~~
tomp
I think the original idea behind compile-time evaluation is that it simplifies
metaprogramming (compared to e.g. templates or macros), because there's only
one set of evaluation rules - so the language works the same way, it's just
that some parts of the program are run at compile time.

~~~
pulse7
Compile-time evaluation is for faster startup (initialization is done at
compile time).

------
partycoder
This is a tetris clone made in zig
[https://github.com/andrewrk/tetris](https://github.com/andrewrk/tetris)

I bit hard to follow without the syntax highlighting, but seems a good example
of what a larger application would look like.

~~~
AndyKelley
Here's an even better example: [https://github.com/Hejsil/pokemon-
randomizer](https://github.com/Hejsil/pokemon-randomizer)

Also here's a game:
[https://gitlab.com/dbandstra/oxid/](https://gitlab.com/dbandstra/oxid/)

------
aey
What I want in a systems language is heap allocations that are typed. A Monad
basically to wrap the heap context.

------
bitL
Heh, property keyword was my favorite in Delphi/C# and I really missed it in
C++. So Zig is not for me.

------
camdenlock
I have nothing against Zig, but... am I the only one feeling "new programming
language fatigue" lately? Seems like there are several new ones per week these
days.

~~~
Caster9
It's getting easier to build them.

~~~
richardwhiuk
It's not - it's getting harder to build worthwhile ones.

The barrier to entry is higher, the expectations on the size of the standard
library are higher, the expected quality of tools is higher.

~~~
andrewflnr
But it's getting easier to just implement a language at all. That might be all
you need to hit HN. No one said anything about "worthwhile" ones.

~~~
mmirate
That says a lot more about HN than about anything else...

~~~
andrewflnr
It says that we look for more in an idea than pure utility, that cleverness
and beauty also count. That's a good thing.

~~~
mmirate
> That's a good thing.

Maybe, maybe not, but luckily that question's not very important. Here's what
is:

Where is the cleverness and beauty in _Yet Another Boring LLVM Frontend_ (tm)?
Zig and Rust are more than that, I'll readily grant - but that description
does fall under the umbrella of "implementing a language".

