But i would not put comptime as some sort of magical invention. Its still just a newish take on meta programming. We had that since forever. From my minimal time with Zig i kind of think comptime as a better version of c++ templates.
That said Zig is possibly a better alternative to c++, but not that exiting for me. I kind of dont get why so many think its the holy grail, first it was rust, and now zig.
Rust's borrow checker isn't unique either but was inspired by Cylone: https://en.wikipedia.org/wiki/Cyclone_(programming_language)
IMHO a programming language doesn't need a single USP, it just needs to include good existing ideas and (more importantly) exclude bad existing ideas (of course what's actually a good and bad idea is highly subjective, that's why we need many programming languages, not few).
When the author mentioned manually wiring up a PATH variable with gushing excitement I finally knew what I was up against. Holy fuck. Somebody please introduce the poor soul to UNIX because I’d rather someone pitches a tent over a cool NetBSD function or something. That would be a nerd article worth getting a box of Kleenex for.
You can switch one out for any of the others and the article would be the same.
I’m holding out for that one fucking moron who writes the next “Why I’m still using Lua in 2025” and we find out the punch line is he’s making 10M ARR off of something so dumb it shouldn’t even be possible.
By the way, so does everyone using neovim.
See also Roblox (and there used to be a whole bunch of game engines that had Lua scripting but I -think- most of them have switched to "NIH!" scripting engines?)
Powerful macros that generate code that then gets compiled =)
Definitely.
https://dlang.org/spec/function.html#interpretation
It doesn't need a keyword to trigger it. Any expression that is a const-expression in the grammar triggers it.
Constant-folding just got watered down by the many dynamic evangelists in the decades after, that even C or C++ didn't enforce it properly. In perl5 is was watered down on add (+) by some hilariously wrong argumentation then. So you could precompute mult const expressions, but not add.
The same is true for templates, or macros—all of which are distinguished by being computed in a single pass (you don’t have to think about them later, or worry about their execution being interleaved with the rest of the program), before runtime start (meaning that certain language capabilities like IO aren’t available, simplifying reasoning). Those two properties are key to comptime’s value and are not provided by perl5’s BEGIN blocks—or probably even possible at all in the language, given that it has eval and runtime require.
When you want to use state, like openening a file for run-time, use INIT blocks instead. These are executed first before runtime, after compile-time.
My perl compiler dumps the state of the program after compile-time. So everything executed in BEGIN blocks is already evaluated. Opening a file in BEGIN would not open it later when required at run-time, and compile-time from run-time is seperated. All BGEIN state is constant-folded.
I know who you are, and am sure everything you say about the mechanisms of BEGIN is correct, but when I refer to “compile time”, I’m referring to something that happens before my program runs. Perl5’s compilation happens the first time a module is required, which may happen at runtime.
Perhaps there’s a different word for what we’re discussing here: one of the primary benefits of comptime and similar tools is that they are completed before the program starts. Scripting languages like perl5 “compile” (really: load code into in-memory intermediate data structures to be interpreted) at arbitrary points during runtime (require/use, eval, do-on-code).
On the other hand, while code in C/Zig/etc. is sometimes loaded at runtime (e.g. via dlopen(3)), it’s compile-time evaluation is always done before program start.
That “it completed before my code runs at all” property is really important for locality of behavior/reasoning. If the comptime/evaluation step is included in the runtime-code-load step, then your comptime code needs to be vastly more concerned with its environment, and code loading your modules has to be vastly more concerned with the side effects of the import system.
(I guess that doesn’t hold if you’re shelling out to compile code generated dynamically from runtime inputs and then dlopen-ing that, but that’s objectively insane and hopefully incredibly rare.)
For example Zig has a function ArrayHashMapWithAllocator which returns well, a hash table type in a fairly modern style, no separate chaining and so on
Not an instance of that type, it returns the type itself, the type didn't exist, we called the function, now it does exist, at compile time (because clearly we can't go around making new types at runtime in this sort of language)
The issue with mixins is that using string concatenation to build types on the fly isn't the greatest debugging experience, as there is only printf debugging available for them.
The parent comment acknowledges that compile time execution is not new. There is little in Zig that is, broad strokes, entirely new. It is in the specifics of the design that I find Zig's ergonomics to be differentiated. It is my understanding that D's compile time function execution is significantly different from Zig's comptime.
Mostly, this is in what Zig doesn't have as a specific feature, but uses comptime for. For generics, D has templates, Zig has functions which take types and return types. D has conditional compilation (version keyword), while Zig just has if statements. D has template mixins, Zig trusts comptime to have 90% of the power for 10% of the headache. The power of comptime is commonly demonstrated, but I find the limitations to be just as important.
A difference I am uncertain about is if there's any D equivalent for Zig having types being expressions. You can, for example, calculate what the return type should be given a type of an argument.
Is this a fair assessment?
This is done in D using templates. For example, to turn a type T into a type T:
template toPtr(T) { alias toPtr = T*; } // define template
toPtr!int p; // instantiate template
pragma(msg, "the type of p is: ", typeof(p));
The compiler will deduce the correct return type for a function by specifying auto* as the return type: auto toPtr(int i) { return cast(float)i; } // returns float
For conditional compilation at compile time, D has static if: enum x = square(3); // evaluated at compile time
static if (x == 4)
int j;
else
double j;
auto k = k;
Note that the static if does not introduce a new scope, so conditional declarations will work.The version is similar, but is intended for module-wide versions, such as:
version (OSX)
{ stuff for OSX }
else version (Win64)
{ stuff for Windows 64 }
else
static assert(0, "unsupported OS");
Compile time execution is triggered wherever a const-expression is required. A keyword would be redundant.D's mixins are for generating code, which is D's answer to general purpose text macros. Running code at compile time enables those strings to be generated. The mixins and compile time execution are not the same feature. For a trivial example:
string cat(string x, string y) { return x ~ "," ~ y; }
string s = mixin(cat("hello", "betty")); // runs cat at compile time
writeln(s); // prints: hello,betty
I'll be happy to answer any further questionsFor example, the creator of Odin, has stated in the past he rather come up with optimal solutions without metaprogramming, despite enthusiasts trying to pressure him to add such features into that language.
Mostly what I think is the syntax is more complex with less utility than the equivalent D syntax. For example, the use of the 'comptime' keyword is not necessary. For another, the import declaration is overly complex.
I don't know enough about Zig to make informed suggestions on evolving it. D has borrowed stuff from many languages, but I don't recall suggestions in the D forums of a Zig feature that should be added to D, though I might have missed it.
Pointing out that other languages have used partial evaluation, sometimes even in ways that somewhat overlap with Zig's use, completely misses the point. It's at least as misplaced as saying that there was nothing new or special about iPhone's no-buttons design because touch screens had existed since the sixties.
If you think Zig's comptime is just about running some computations at compile time, you should take a closer look.
In addition, there's the classic example of implementing a parameterised print (think printf) in Zig. This is a very basic use of comptime, and it isn't used here in lieu of generics or of interfaces, but while there may be some language that can do that without any kind of explicit code generation (e.g. macros), there certainly aren't many such examples: https://ziglang.org/documentation/0.15.2/#Case-Study-print-i...
But the main point is that the unprecedented use of partial evaluation is in having a single unified mechanism that replaces generics, interfaces, and macros. If a language has any one of them as a distinct feature, then it is not using partial evaluation as Zig does. To continue my analogy to the novel use of a touchscreen in the iPhone, the simplest test was: if your phone had a physical keypad or keyboard, then it did not use a touchscreen the way the iPhone did.
write(1,2,"abc",4.0,'c');
write is declared as: void write(S...)(S args) { ... }
where `S...` means an arbitrary sequence of types represented by `S`. The implementation loops over the sequence, handling each type in its own individual fashion. User defined types work as well.It's like saying the the iPhone design wasn't novel except for the fact that prior art all had a keypad. But the design was novel in that it was intended to eliminate the keypad. Zig's comptime feature is novel in that it exists to eliminate interfaces, generics, and macros, and you're bringing up a language that eliminates none of them.
So D clearly isn't an example, but perhaps there's some other language I haven't heard of. Just out of curiosity, can a printf in D not only check types at compile time but also generate formatting code while still allowing for runtime variables and without (!!!) the use of string mixins? Like I said, it's possible there's precedent for that (even though it isn't the distinguishing feature), and I wonder if D is that. I'm asking because examples I've seen in D either do use string mixins or do not actually do what the Zig implementation does.
It's like how the novelty of the iPhone's touchscreen design was in not having a keypad, or that the novelty of the spork wasn't in inventing the functionality of either the spoon or the fork, but in having a single utensil that performs both. The more important aspect isn't the functionality but the design. I'm not saying you need to like any of these designs, but they are novel.
Saying that you could have a similar functionality by other means misses the point as much as saying that there's nothing special about a spork because if you have a spoon and a fork, then you have the same functionality. But you still don't have a spork.
You could, then, ask what the point of the novel design is. Well, in some languages you have generics and interfaces and compile-time expressions, but because none of these is general and powerful enough, so you also have macros. Macros are very powerful - perhaps too powerful - but they are difficult to understand, so they're used sparingly even if they can subsume other functionality.
Zig has shown that you can do almost anything you would reasonably want to do with macros with partial evaluation that has access to reflection. That wasn't obvious at all. And because that feature was not only powerful enough to subsume other features and make them redundant, but also very simple and easy to understand, it ended up with a design that is both minimal and easy to read (which is important for code reviews) but also highly expressive. Again, you don't have to like this rather minimalistic design, but it is novel.
Please show an example of Zig partial evaluation.
Again, the novel use of partial evaluation in Zig is that it eliminates generics, interfaces, and macros. Therefore, any language that has one or more of these features does not have this novel design.
I mean a simple example. Just to illustrate the concept. Like the examples I provided here:
> But the extent and the way in which Zig specifically puts it to use -- which includes, but is not limited to, how it is used to replace other features that can then be avoided (and all without macros) -- is unprecedented.
That MrWhite wanted to knkw an example of Zig's comptime that is not merely a "macro", rather the usage as a replacement of other features (I guess more complex..)
PS just interested in zig, I'd like some pointer to these cool feature :)
-- Alan Kay
> yours has zero value
Yours didn't bring much as well, so I suppose value isn't strictly required.
BTW: Your reply made me laugh so hard… I loved it.
Thanks.
The reason is the clickbait title.
No such thing. Also C++ has most of those features too.
It has been several decades since putting a slash between these two made sense, lumping them together like this. It would be similar to saying something like Java/Scala or ObjectiveC/Swift. These are completely different languages.
Indeed you see those for Java/Scala and Objective-C/Swift in technical books and job adverts.
Any search on the careers sites, or documentation, on companies that have seats at ISO, sell/develop C and C++ compilers, have such C/C++ references in a couple of places.
Do you need any example?
It is a bikeshedding discussion that doesn't help in anything, regarding lack of security in C, or the legions of folks that keep using C data types in C++, including bare bones null terminated strings and plain arrays instead of collection types with bounds checking enabled.
Even better, all heap allocations should be done via ownership types.
Calling into malloc () is writing C in C++, and should only be used for backwards compatibility with existing C code.
Additionally there is no requirement on the C++ standard that new and delete call into malloc()/free(), that is usually done as a matter of convenience, as all C++ compilers are also C compilers.
And this is exactly the stance I am arguing against. C++ is not the newer version of C. It forked of at some point and is a quite different language now.
One of the reasons I do use malloc for, is for compatibility with C. It is not for backward compatibility, because the C code is newer. In fact I actively change the code, when it needs a rewrite anyway, from C++ to C.
The other reason for using it even when writing C++ is, that new alone doesn't allow to allocate without also calling the constructor. For that I call malloc first and then invoke the constructor with placement new. For deallocating I call the destructor and then free. This also has the additional benefit, that your constructor and deconstructor implementation can fail and you can roll it back.
Finally, people like to argue between C and C++ when it convenient to do so, yet the compiler language switches to use C extensions in C++ mode keep being used across many projects.
What do you mean? I don't think I can follow you.
> yet the compiler language switches to use C extensions in C++ mode keep being used across many projects.
When you use compiler extensions, that just happen to be both available in C and C++, I wouldn't say you are writing C in C++, I mean the extension isn't standard C either.
Code written in C++ has different semantics, even when it is word-for-word the same as C code. They ARE different languages.
That is what writing proper modern C++ is all about, anything else is writing C in C++.
Null terminated strings with pointer arithmetic instead of std::string and string_view, pointer arithmetic instead of std::span, bare pointer arrays instead of std::array and std::vector, C style casts,....
That is a claim that is yours and I do not agree with that. C++ that does not fit your taste of modern C++ does not suddenly become C, it is likely a syntax error in C and when it compiles it has a different meaning. Code that may look to you like C in C++ has C++ semantics, that differs from C semantics.
Class * object = new (...);
I do: Class * object;
object = malloc (sizeof Class);
if (nullptr == object) // ... error handling
new (object) Class (...);
or if the constructor needs to fail: Class * object;
object = malloc (sizeof Class);
if (nullptr == object) // ... error handling
new (object) Class ();
if (!Class::init (object, ...)) {
object->~Class ();
free (object);
// ... other error handling
}The pedantic folks that jump of their chair when seeing something all companies that pay WG21 salaries use on their docs?
If only they would advocate for writing safer code with the same energy, instead of discussing nonsense.
That is why C and C++ communities are looked down by security folks, and governments.
I'd say the same thing about Rust. I find it the best way to express when what code should run at any given point in the program and the design is freakin interstellar: It is basically a "query engine" where you write a query of some code against the entire available "code space" including root crate and its dependencies. Once you understand that programming becomes naming bits and then queries for the ones you wish to execute.
Yeah, I know nothing about Zig, and was excited by the author's opening statement that Zig is the most surprising language he has encountered in a 45 yr software career...
But this is then immediately followed by saying that ability to compile C code, and to cross-compile, are the most incredible parts of it, which is when I immediately lost interest. Having a built-in C compiler is certainly novel, and perhaps convenient for inter-op, but if the value goes significantly beyond that then the author is failing to communicate that.
It seems like this is written from the perspective of C/C++ and Java and perhaps a couple of traditional (dynamically typed) languages.
On the other hand, the concept that makes Zig really unique (comptime) is not touched upon at all. I would argue compile-time evaluation is not entirely new (you can look at Lisp macros back in the 60s), but the way Zig implements this feature and how it is used instead of generics is interesting enough to make Zig unique. I still feel like the claim is a bit hyperbolic, but there is a story that you can sell about Zig being unique. I wanted to read this story, but I feel like this is not it.