Zig is an attempt to merge metaprogramming and type system into a single, coherent unit, however not without drawbacks. Basically, in an ideal language, I see that type system permits three kinds of dependencies:
- Values dependent on values (algorithms)
- Values dependent on types (parametric polymorphism)
- Types dependent on types (type-level functions)
This is one dimension weaker than Calculus of Constructions (CoC) [1], which permits types dependent on values (so-called "dependent types"), but we don't need them until we want to prove some properties about our code (dependent types correspond to logical predicates). Some tasks handled by metaprogramming however may not be handled even by a type system that permits type-level functions; for this reason, probably we have to include some sort of simple syntax transformations, such as those found in Idris [2].
I expatiated in one of my posts [3] on why static languages duplicate their language concepts. This writing also includes Zig and Idris.
Thank you for your well written article. It plainly describes thoughts that I've had yet struggled to put concretely in words.
You mentioned the complexities of idris and the lack of adoption for a unified system. Are you hopeful that programming languages will begin to move in this direction, or do you think we will quickly arrive at a "good enough" halfway house (Rust seems like a good example of this)?
I'm afraid I can't answer your question. Either scenario -- a unified system or a "halfway house" -- might be the case; it depends on the intentions of people who design programming languages and the number of pretty arbitrary factors determining their success. But since the general trend is that the PL community is heading towards more advanced type features, maybe someday we should see such a language.
By the way, I don't think it's time to claim that Rust is a "good enough" house -- simply too little time passed since it was embraced by the community. The language didn't even include `async`/`.await` some four years ago. Macros 2.0 are being developed; a new trait solver too. If after some ten or twenty years we'll find it being a mess of legacy, maybe it is not the right way to design a language.
just so that suitably sized vectors can be used as records:
STRUCT POINT = PX, PY, PCOLOR; ! same as CONST PX = 0, PY = 1, PCOLOR = 2, POINT = 3;
VAR p[POINT]; ! same as VAR p[3];
p[PCOLOR] := 42; ! same as p[2] := 42;
It works reasonably well and actually allows for somewhat better meta-programming than e.g. C with its X-macros because you can literally loop over a struct's field, in a normal FOR loop.
It seems like this only works with dynamic typing or if you assumed every field was the same width? Otherwise you can't actually specify the widths of each of the struct fields.
Perhaps TypeScript is a very clean example of the duplication phenomenon you mention in that article, because unlike in most languages, static types are completely erased during compilation. It’s literally a typecheck-time functional programming language bolted onto an existing dynamic runtime language. For some complex object manipulations, the cleanest thing to do is to write two independent recursive functions: a runtime one to compute the value, and a typelevel one to compute the resulting type.
Type erasure is a specific compilation technique, it means more than a lack of introspection capabilities.
In most languages, a function taking a float and a function taking an int end up producing different compiler output (because they need to use different CPU registers). More to the point, this extends to generic/template functions with signatures like “void foo<T>(T arg)”, which then need to be monomorphized, that is, copied for each instantiation.
Languages with type erasure simply drop the type argument instead of instantiating it.
What kinds of metaprogramming cannot be expressed via dependent types? AIUI, dependent types potentially dispense completely with the phase separation found in languages like C, so anything can be "evaluated at compile time" simply by writing ordinary programs that make sense within the language itself. Of course sometimes it is desirable to reintroduce a separate "run time" phase, but this can be done via program extraction.
In general, dependently typed systems lack the ability to metaprogram at the syntactic level. This is particularly true we’re binding forms are concerned. The most thoroughly pursued type theoretic way to do this is to implement a modal type system, which is technically orthogonal to the dependency capabilities of the type system, but integration of the two is tricky.
What're the failings of elaborator reflection in providing this in your view? I haven't used it thoroughly, but I thought one could define custom binding forms with it (in Idris 1, at least).
D templates can have "constraints", which are composed of conventional code that is executed at compile time to see what values and types are acceptable. It is not necessary to compile the template implementation looking for errors. The constraints also enable template overloading.
But are templates guaranteed to compile without errors for any arguments that match the constraints? If not, then D is in the same situation as C++ with concepts: there is always the possibility of having to debug errors from the template implementation because the authors didn’t choose precise enough constraints. Rust, Go, Haskell, etc. do guarantee successful compilation, though at the cost of making their generics systems far less suitable for metaprogramming.
> there is always the possibility of having to debug errors from the template implementation because the authors didn’t choose precise enough constraints
That is indeed possible, but it's hard to see why that is a problem. Any problems would show up at compile time, and fixing errors flagged by the compiler is pretty routine programming work.
Unfortunately C++20 only gave us concepts light, however Circle might be getting full concepts as they were originally designed, and those are like Rust, Go, Haskell, etc.
There's a core contingent of ISO members who don't like (don't understand?) `concept_map`; without late (and layered) binding of types to concepts you're really just rolling out a template prediction scheme.
I mainly use C++ on side projects and native library bindings, somehow I think ISO will need to change their ways if they want people to still care about anything past ISO C++23 for green field projects.
Usually when something is called "meta" it means that it is special and does not compose.
I had someone explaining D templates to me on an Idris meetup, but it's been a few years, so I might very well be wrong here and not remember correctly. I'm happy to be corrected.
My next question would be: can I write those meta templates myself and modify them with themselves? In the same way that I could have a type-level function that applies another type-level function twice and then apply it on itself to get a new type-level function that applies another type-level function four times.
I wasn't talking about recursion but composition. Can I write template A and then apply it to itself to get template B and then apply that onto template C to get template D.
alist
.sort
.map!(x => x * x)
.map!(x => x - x/2)
.map!(x => x + 1);
This seems like what you're asking. Each of those is a template. And while this is all logic code, D has inferred the type aka: SortedRange!Map!Map!Map
> How does Zig handle the monomorphization problem for generics in library APIs?
From the article:
> Cannot distribute binary-only libraries with generic interfaces … Zig doesn’t have this constraint because Zig is designed around whole-program compilation from the start …
So no proprietary (EDIT: (closed)) libs built with Zig, I guess.
Proprietary is a licensing thing, and doesn’t really have to have much to do with the form of the files comprising the software. Plenty of proprietary software gets written in Javascript, for example.
You're going to lose a lot of zig safety and type tracking features that way. I think it's possible that there will somehow be an exportable library abi in zig, but it is not at all on the roadmap. In any case, I don't believe there are obvious ways (I do know sneaky ways to do this) to ship post-compiled-only libraries this any of the programming languages I use on a day-to-day basis and even with venerable C, the "single header library" is all the rage these days
I can even imagine a tool that picks a zig interface and transforms it into a C interface to export it from your closed-source library, and a shim that exposes the nice zig interface that talks to the uglified C interface.
That would be helpful not just for intetfacing closed zig code, but also for dynamic libraries in zig.
I'm only afraid that in many cases that would require doing the monomorphisation step...
You have to be able to export a binary interface of some sort to be able to write operating system components. As Zig is intended for system programming, the Zig implementers will surely figure this out eventually.
No it doesn't, it is just like most compiled languages, the ABI is the compiler version.
Ada packages, just like most module based languages have the necessary metadata on the libraries themselves, no need for header files.
Just like C doesn't have a stable ABI per se, there is some overlap with code produced by C compilers with the OS ABI, when the OS happens to be written in C, and that is about it.
> So no proprietary (EDIT: (closed)) libs built with Zig, I guess.
And frankly, that's a good thing. Working with libraries that are only distributed as a binary blob and header files is a pain in C++ and even C, even something as simple as different compilation settings may introduce all sorts of weird problems. If distributing your plain source code is an issue, then use a code obfuscator. That's just as "safe" as distributing binary blobs, but still much easier to integrate into a project.
An aside: Swift’s generics have a pain point I bump into nearly daily, where type aliases are just that-aliases of the underlying type. This means that protocol conformance/method implementation is applied to the underlying type as well as any aliases.
So if I have two types, both of which can be represented by an underlying Int, but have entirely different semantics or methods (for instance, LosslessStringConvertible or various computed fields based on the underlying Int), I have to wrap the underlying type in either an enum with a single case or a struct with a single field, obscuring the meaning of the type and requiring awkward duplicate naming of the struct field/enum case.
The ExpressibleBy*Literal protocols are some help here, allowing Int/String/Array literals to be directly assigned to the type, but this only helps for assignment and not for retrieving the underlying value (and only
applies when the type can be initialized from the literal without any failure cases).
It’s not a huge deal, but a simple newtype statement would make the generics system far more general and easy to use imo.
Only of the design goals of Swift is precisely not to be as low level as Rust, unless required to for high performance code, and had a few big names from C++ community involved in its initial versions.
True. I wouldn’t want to think about the borrow checker writing UI code, and I don’t need the performance or ultra-optimized machine code of Rust when I write Swift.
I moreso meant in terms of the high-level features of Rust and how they integrate FP-style patterns into a C-style imperative language. Like tuple-based structs as discussed above, or control flow as expressions, like if and match.
The alternatives to the latter in Swift are
A) defining a dedicated function for the control flow, using return statements
B) defining but not initializing a let constant before the control flow statement
C) using the ternary operator, which is concise but doesn’t really allow for destructuring complex types
all of which are more verbose, less clear in intent, and IMO don’t embody’s Swift’s mantra of “clarity at the site of use” as well as Rust’s approach.
Practically, there’s little difference. But as they say, naming things is one of the two hard problems in computer science and I’d rather avoid it wherever possible when the name inevitably doesn’t add any meaning, and arguably obscures it.
I could just use rawValue as the field name every time, but foo.rawValue is a lot less readable, IMO, than foo.0
You can define a struct with a single field in Swift, which is guaranteed to have the same representation as the field's type (but yeah, you have to "forward" the desired subset of protocol conformances manually to the underlying type).
I feel that this post conflates two different things:
1. The model for expressing and instantiating a generic function.
2. The model for constraining a generic function such that type errors are as
local and comprehensible as possible.
Zig currently doesn't do #2 at all, true. Neither did C++ until C++20. Doing #2 clearly has benefits, as the article effectively argues. Those benefits can be realized later by adding constraints later, like C++ did.
When people argue that Zig made the right decision for generics, and that other languages should follow suit, they are talking about #1, not #2. To convince me that "Zig-style generics are not well-suited for most languages", you would need to convince me that Zig's choice for #1 will make it difficult or impossible to do #2 later.
By #1 I mean Zig's design of expressing all generic-related logic using normal control flow constructs that just happen to be evaluated at compile time.
For example, suppose you want a generic function or generic type to have some special cases for certain types. In C++, you would write a template specialization, which is a special syntax. In Zig you would just write an if() statement: https://ikrima.dev/dev-notes/zig/zig-metaprogramming/#dynami...
I think that what the author is suggesting is that you still have an open world problem that causes #2 to rear its head eventually, due to #1.
You can try to use plain syntax to narrow types, but the only way to head off the gnarly errors is to close the universe of type parameters. That turns out to be a pretty weak form of generics.
I don't think you can call Zig's choice for #1 the cause of the problems in #2. C++ made a different choice for #1 and yet still suffered the problems of #2 until C++20.
You can't use "if constexpr" to vary the type signature, for example (as in my link). Or to choose a different representation for a type based on a type parameter. Those will require metaprogramming at the template level.
Another example: you can't write std::is_same_v itself using "if constexpr".
Speaking from extremely limited Zig experience here: I think some of these downsides will get clearer over time as Zig becomes more popular. Classic C++ issues like "wtf does this 500-line compiler error mean" aren't a big deal when you're working on programs that are entirely written by you. You probably know what you did wrong, and what you expected yourself to do instead. It's when you have large applications maintained by rotating teams of programmers, or big open-source library ecosystems where everyone is using tons of other people's code, where it starts to be a bigger problem, especially for beginners.
One concrete way this stood out to me is that I don't think Zig has any way to say "this function should take a Writer". (I.e. a File or a Socket or something you can stream bytes into.) Zig does have a Writer abstraction in the standard library, which takes a basic write function and provides lots of helper methods like writeAll. However, the Writer abstraction gives you a new struct type, and while you could write a function that takes a specific type of Writer, I don't think there's any way to say "any Writer". Instead you usually have to take "anytype".
I think things like the Writer interface in Go or the Write trait in Rust are very powerful and useful for organizing an ecosystem of libraries that all want to interoperate. This might be something Zig struggles with in the future. On the other hand, a lot of Zig's design seems targeted at use cases where you don't necessarily want to call lots of library code (which for example might be allocating who-knows-how-much memory from who-knows-where). In use cases like kernel modules and firmware, there might be an argument that making lots of fancy abstractions isn't the best way to go about things. I'd love to hear more about this from more knowledgeable Zig folks.
> doesn't have any way to say "this function should take a Writer"
AFAIK there isn't any dedicated pretty syntax, but there are plenty of solutions that aren't too painful. They all rely on the fact that you're not generating a "new" struct with each call -- comptime function calls assume that the same inputs will yield the same outputs and cache the results to enforce that behavior, so if you call Writer(foo) in two different places you get the _exact_ same struct in both places.
One option would be to instantiate the Writer type just as you would dependent generics in any other language.
Another option would be to just check the type at comptime. You're able to throw custom error messages during compilation, and comptime code doesn't add runtime overhead of any sort.
For completeness, it's worth noting that the correct behavior for strings and more complicated objects is still AFAIK a subject of debate with respect to comptime caching. It should have some nice solution by 1.0 in a few years, but for primitives like functions and types you should have no issues with either of the two above approaches.
> don't necessarily want to call lots of library code
Makes sense since Zig is meant to be a replacement for C and "C is NOT a modular language! The best way to use C is to roll your own (or copy and paste) data structures for your problem, not try to use canned ones!" from a comment here: https://news.ycombinator.com/item?id=33130533
Edit: I call this misfeature of the C language the "polluted/busy call site syndrome".
This may be the case with the current Zig ecosystem (even then, two community-created package mangers already exist), but my understanding is that at some point, Zig will receive an official package manager.
The current build system and type system go a long way to encourage library use (since it's quite easy) and the future package manager will be yet another step towards that.
Oh that's fair. Zig makes a big deal about ensuring that "what you're supposed" to do is the simplest/easiest option at your disposal.
In service of this, even in Zig's current pre-1.0 state, adding a library can be as simple as something like the following in your project's build.zig:
This and the language just having generics (which isn't necessarily the goal of all c-replacement languages i recently found out) suggests to me that the language as it currently stands encourages libraries to be written and reused.
In Zig, allocators are "just another argument", functionally an interface so as a library author you have to pay less attention to whether your library can be used in hostile environments. I'm quite sure this idiom exists primarily to just make Zig libraries (like the stdlib) useful in more places.
Certainly, Zig doesn't have all the tools you'd expect in other languages to aid library authors and consumers. I personally would love to see proper interfaces in the language, rather than the interface-by-convention situation we have right now. It's a matter of tradeoffs, many of which I imagine will be addressed and reconsidered as the language matures.
I think some of this can be achieved in the function body by checking the incoming generic types via Zig's comptime type inspection features (see: https://ziglang.org/documentation/master/#typeInfo). Kinda like 'comptime type-asserts'. Together with @compileError this would at least provide more useful error messages if a generic type doesn't have the expected features.
PS: I think it's important to not look at Zig's generic feature in isolation, instead it goes hand in hand with comptime and reflection.
...there's probably more advanced helper functions in the same std.meta.trait module, I haven't explored that yet.
Also, disclaimer: I haven't dabbled much yet with Zig generics, don't know if this is the proper style (in this case it's probably better to get rid of T entirely and just use anytype for the obj argument).
They are called trade-offs. They are still a problem in Zig but the language is designed to work around them. If your language doesn't work around them you will suffer a lot more from those problems. If your language works around them it is basically Zig but less polished and far less used.
Because in typical Zig code, generics are often used to solve relatively basic problems, and not to play code golf. For instance, if you just want to write a container for a generic type T you don't need any advanced features.
Also, quite a few problems where people would reach for template code in C++ can be solved much easier in Zig with a combination of regular comptime code and only some 'generic typing' sprinkled over.
I'm wondering if Zig's generics are a good way forward nonetheless? Much like TypeScript added type-checking to JavaScript, a future version of Zig could add type constraints that can be used on comptime variables to catch common errors.
(They probably won't be sound type constraints, but maybe that's not that big a deal, since the result would be a worse compile error at instantiation time.)
Soundness is of relatively minor importance on account of rejecting the occasional correct program is a reasonable price to pay for rejecting all incorrect programs. Especially when you can allow the programmer to hint the analyzer.
> rejecting the occasional correct program is a reasonable price to pay for rejecting all incorrect programs
I agree with you, but the C++ language disagrees very strongly, the set phrase in the C++ standard is "Ill-formed, no diagnostic required". ie the compiler isn't required to do anything sane in this case, but it also isn't required to emit a warning or error.
Really interesting. It is really good to go through pros and cons of certain decisions like this.
I really don't like how all of these camps have started shouting at each other. I really like both Rust and Zig and think both of their approaches are super interesting paths forward for the industry. They are both re-examining things that has been very locked in place for a long time in the industry. Zealotry on either side is super cringe.
In Zig's case it is good to look at the choices made in the type system of other languages. What do we loose and what do we gain over these? There were probably reasons why they went the way they did. Learn from it. Don't shout that our way is the best and don't assume that you were the first to find this perfect solution to all type problems. Both of those reactions are just childish.
I'm perplexed by the motivation of this article: who's giving "armchair suggestions about how other languages should embrace Zig-style generics" and why should anybody care? If it's a bad idea, and someone tries to implement it, they would find out by themselves: there's no need to attack them for their different tastes.
And clearly it is only a matter of different tastes (i.e. wanting all languages to be just about the opposite of Zig/C++/D etc.): the choice of traditional compilation is ridiculed rather than understood as a deliberate and useful limitation and the worst accusation against "templates" is that they are inelegant and that they lead to messy error messages, not that they are limited.
Issue with unconstrained template-based approaches: Only static dispatch is possible which means, the compiler has to duplicate the procedure for all actual type-combinations and a procedure cannot return objects of different types, even though your are only interested in the interface they implement.
Well, you could if you either ensure, that the layout of the inherited part of a structure is the same for all structures, that inherit the same structure. Or you could return a tagged enum. However, with interfaces or traits, you just have to ensure that the table of interface/trait-methods of different types, that implement the same interface/trait. This table is constant and therefore a reference to it can be shared by every instance of the interface/trait.
Yes, it might be slower, because instead of direct method-calls, you have a layer of indirection, but you only need one copy of the method, not one for every type-combination. If all copies of this method have to fit in the L1-cache simultaneously but cannot, it might actually be faster.
> There are broadly two different design decisions possible here:
> - Require specifying what operations are possible in the function signature. These can be checked at call-sites of myGenericFunc. There are various approaches to this – Go interfaces, Rust traits, Haskell type classes, C++ concepts and so on.
> - Don’t require specifying the operations needed up-front; instead, at every call-site, instantiate the body of myGenericFunc with the passed type arguments (e.g. T = Int, S = Void), and check if the body type-checks. (Of course, this can be cached in the compiler to avoid duplicate work if one has a guarantee of purity.)
As a C++ programmer by trade, its interesting to see C++ as a language go from the 1st approach (via inheritance) to the 2nd approach (via templates). The 2nd approach feels weirdy dynamic, and it's interesting to see a return to the first approach with C++20's concepts.
C++ 20 Concepts are still just duck typing, although the syntax is less awful than with SFINAE. C++ 0x Concepts were something more, but those were never actually standardised, and as Sean Baxter found it's difficult to piece together what exactly was intended in the never-finished standard.
The big give away is, can I use this to care about semantics or just syntax ?
More over though, although C++ 20 Concepts are checked early (which can result in better diagnostics) they are not constraints on how the type is actually used.
Suppose we have a library function with a parameter v, whose type is constrained to be a Bird. The Bird concept says it has a flap_wings() method, and a predicate named swim() which checks whether this bird can swim. Our function does use v.flap_wings() but it also calls v.chirp() a function which is not defined for the Bird concept.
The compiler is fine with this, as it stands, our library is good. Users shall call this function with a parameter v which is a Bird. Secretly the compiler knows that as well as being a bird, v.chirp() needs to work, but that's OK, no doubt all users will arrange this. Right? Perhaps it will be documented somewhere.
When a user of our library tries to use an Ostrich, which is indeed a Bird because it has flap_wings() and the swim() predicate, the compiler doesn't like that. Somewhere in the dozens of lines of diagnostics is the fact that Ostrich doesn't have a chirp() method and so it's not suitable despite being a Bird. This is probably not what we, as library authors, intended. Too bad.
You can fix that with some static analysis/IDE tooling help, giving an error when the implementation makes use of stuff not declared on the concept requirements.
Interesting. I haven't actually used concepts yet. Kind of sounds like type hints for generics, in the same way that type hints for e.g. Python are very best-effort rather than actually sound.
It is not unrelated. If a is an abstract class, and f takes a pointer to an a, I can pass it a b, for any b which inherits from a; the implementation of b is up to me.
In the context of generics/templates, it is unrelated. GP is right.
C++ templates are duck typed, but with concepts they're now moving towards static contracts on parameters, like rust's trait bounds. No relation to inheritance or the notion of class.
Is my understanding incorrect that classes isn't an example of the first approach?
"Require specifying what operations are possible in the function signature." sounds a lot like classes, interfaces and inheritance to me, as a function:
void foo(Bar b);
Ensures that the operations Bar::* exist in the function signature.
You’re right that classes exemplify declaring requirements upfront; the GP is limiting scope to what happens at compile time. Taking an argument of class Bar typically means taking pointers at runtime to a subclass’ method implementations.
Of course, optimizing compilers (especially JITs) can often devirtualize these anyway, so it’s not even that clear-cut.
> The C++ committee added concepts after decades of pain with templates, and even these do not overcome all the problems mentioned above. I would not be surprised to see Zig add a similar system (likely with its own twist to it) at some point if it keeps growing in usage.
Templates in C++ are quite different because of overload candidate resolution (and so SFINAE), which makes debugging templates a pain. Zig doesn't have overloads at all, plus, the error reporting mechanism (which C++ doesn't have) is available at compile time, as it is at runtime. So Zig both has better error reporting (or the potential for that) and doesn't have a big source for scary template errors in C++ even before adding concepts.
Having said that, I actually would like to see "concepts" in Zig, probably as a simple boolean predicate as the type annotation in function parameters, for one reason, and one reason only, which isn't mentioned in the article: documentation.
BTW, there's a general trend to compare Zig to some other language and extrapolate from it, but while I understand why people think that's helpful, it ends up just being confusing, because Zig is a very radical language that's not really similar to anything (except superficially). So it's very hard to compare features in isolation, because most problems are caused by an intersection of some features, and pretty much all the interesting features in Zig are different from what people know -- some are "stronger" and some are "weaker."
> The compiler gives you more flexibility, but OTOH if you want library users to have a good developer experience when they make mistakes, you need to do a bunch of extra work.
Yes, but I'd say the difference here, while similar in argument to typed vs. untyped languages, is qualitatively different. The value of finding errors sooner is not as high as the difference between finding errors before a program runs on the user's machine or after. Even if a generic library isn't well-tested (although, any library might not be well-tested) to the point that it has simple type errors, those errors will not find their way into the executable.
Having said that, I obviously agree that Zig's type system is not for all languages, but I'm surprised the article doesn't mention one of the most obvious reasons. Most application programming these days is done in languages that are JIT compiled, and so they don't need partial evaluation (i.e. compile-time evaluation) as much as low-level programming languages do. While some low-level languages have generics/templates and macros and constexprs -- and require distribution of libraries as source code anyway -- many if not most of the popular application languages can do with just generics. Zig's system is definitely much simpler (and personally I find it more elegant) than generics+macros+constexprs, but it isn't simpler than just generics. What Zig discovered was that since low-level programming languages do require partial evaluation, it can be made general and pleasant enough to also do polymorphism (so generics aren't required) and powerful enough to not require macros (which are usually good to avoid, and often require another "language within a language"). So I'd say that if your language does require sophisticated partial evaluation, consider Zig's approach -- it's revolutionary and refreshing; if not, you're probably better off with generics.
comptime and tagged unions are the 2 features i love the most about zig
I'm still not sure if i want to commit with zig, i don't enjoy its ergonomics and error messages (way too noisy, i have to constantly scroll), but whenever i want to implement tagged unions in C.. i just want to drop C and use something nicer
C/C++, Rust, D and even C#, they all don't have them, for C/C++ it's understandable, but for the other ones.. shame on you
Last i checked, Rust tagged union was "unsafe", what's unsafe has no place in Rust [1]
About D, a template is not Tagged Union, it's a template in a module from the standard library, it's not a language feature, it makes me less interested about that language knowing that fact, they follow the same path as C++, ``std::`` crap
You seem to be confused which is understandable. By tagged unions people mean the "enum" data type.
Rust enums are a little different than in other languages that you are likely used to.
They can act like the classic C enums that you are probably used to, where they are more or less just named constants grouped together under a single type.
Then, unlike the classic C style enum, they can also hold data in them, where each variant is basically a struct with named fields and everything. They also support methods just like structs do (the methods are per enum and not per variant). In cases where you don't care about the fields names, you can define the variant as a tuple and later access fields by index.
Struct/Tuple enums are actually implemented as tagged unions by the compiler but this is completely invisible to you so you don't really need to care about it!
Enums are also totally "safe" to create and use!
Rust also has a union type which is like a C union, and you could use it to implement tagged unions (requires unsafe) if you wanted to, but in almost all cases you will just use "enum" for this purpose.
Overloading the "enum" into two different things seems like a strange choice to me but I don't know what the history is behind it, and it takes just a few minutes to figure it out so it's not a huge deal!
enum MyEnum { Ok(u8), NotOk }
fn main() {
let value = MyEnum::Ok(42);
assert!(matches!(value, MyEnum::Ok(_)));
match value {
MyEnum::Ok(x) => assert_eq!(x, 42),
MyEnum::NotOk => unreachable!(),
}
}
No unsafe to be found. The feature is there, it just has a different keyword than you might be used to -- the same way Haskell and Java both have something called "class" that mean different things.
In Rust plain C-style `union`s (used with the `union`) keyword are associated with `unsafe`. The ergonomic, standard replacement is `enum`. So you would do, e.g., `enum Option<T> { Some(T), None }` to represent a nullable value in the type system.
A strength of a language's features is if the user can extend it with library types. Needing special syntactic and semantic sugar is a sign of expressive weakness.
(Though too much expressability can also lead to problems, like adding nitro injection to your car.)
No offense but that's a wrong take on sum types. Sum types should be baked in the same way basic POD structs are, they're that fundamental. That D still doesn't have them as a core construct (along with pattern matching as the core mechanism to inspect the values) is, to me, a weakness inherited from C++ and other 1990s OO-driven languages, and patched in the same insufficient way (see std::variant).
That's true, but in the case of Tagged Union it improves safety, readability and better support for tooling, templates introduce slower compilation and noisy build error, and now you depend on the runtime, it advertise template capabilities of the language for sure, but at what cost..
Why bother with slices when you can have a template? struct Slice(T) { T* ptr; size_t len; }
;)
Another example, this time with Zig, they refuse to have interface/trait, now everyone has to duplicate bogus code, even them in their std, sure it promotes the language capabilities, but at the cost of poor ergonomics, that'll prevent their growth
The D std sumtype is actually super impressive after taking a longer look at it. Crazy how nice it looks to use for something that's not a language construct.
D is the king of practical metaprogramming for me. I think it stands out for compile-time metaprogramming beyond the competition.
Many ppl talk Lisp and Nim and such. They are even more powerful. But I honestly get lost with those and not with D. Idk why, maybe my familiarity with C++ helps.
If you find it easier, you can do the simpler "build a compile-time string and inject it" D-like metaprogramming in Nim with `macros.parseStmt` or `parseExpr`. And just `template` in Nim can go a long ways without all the mucking about with strings.
Nim is choice - in many dimensions. Many things that are built into languages even as flexible as D remain not hard-coded in Nim. E.g., it could be different now, but last I checked, D has hard-coded Associative Arrays. In Nim you can give any user type (e.g. B-Trees) the special syntax (no real meta-programming required..just generics). Also user-defined operators. Etc.
But, yeah - you need to be familiar with a base language whenever you do any meta-programming anywhere. Anyway, if you ever give Nim a try again, maybe `parseStmt` can be your gateway drug. ;-) (But I recommend starting with generics & template...)
I do like Nim also. The only problems are the usual ones plus one:
- IDEs ecosystem (both for D and Nim)
- libraries
Additionally, Nim departs quite a bit from C/C++ to look more than Python. As nice as it looks, Nim is not Python, it has a ton more qualifiers and others. It is fast, but it departs from C/C++ a lot and there is much more than it is apparent to relearn. D's path is smoother in that sense. Also, C/C++ compatibility is a problem.
Yes, I know you could say Nim is very compatible with C. But at the end of the day is a considerable investment of extra work. If it does not work, if it works half-way even if compatible in theory. I have been there many times, I code hybrid in many occassions with scripting layers that are supposed to work, and at the end, there is extra work...
Sorting out your edit/IDE/lib situation & re-learning is part of any PL switch in my experience (& just varies).
Trade offs abound in most things. D is undeniably syntactically closer to C if that is the priority. I knew people in the initial Java era who insisted their next PL had to look like C. I know others who prefer Pythonic brevity and Nim is even more brief & to the point in many ways.
Compatibility-wise, Nim can also just `emit` C/C++/Javascript (like D|Nim can just emit inline assembly) - arguably max compatibility that can even work with arbitrary non-portable backend compiler pragmas, `__int128` extensions, etc. OTOH, that also ties it to limitations of backends. Possibly relevant is https://github.com/nim-lang/c2nim & https://github.com/PMunch/futhark & the rather old/probably partly stale in both columns https://github.com/timotheecour/D_vs_nim .
There are many choices in Nim. Sometimes https://en.wikipedia.org/wiki/The_Paradox_of_Choice can be a real problem - in both software or in life - and at many, many levels. :) I am not trying to challenge your own priorities or push choices on you, but to add information others may be unaware of.
The core of zig is really solid and where systems programing should go. Unfortunately it's a very bike-shedding language that loves to tell you "You're doing it wrong" at really pedantic & annoying levels.
- no multiline comments
- no tabs in source code
- no compiler warnings
- unused variables are a compiler error
It's unlikely to ever change. The Zig community is smart, but it's an echo chamber that only retains the programmers who think all of the above is absolutely fine, and are annoyed at the idea of giving people who program differently to them a choice.
"Tagged union" is a generic non-language-specific term that people who study programming languages use to refer to the language feature that Rust calls "enum". Other languages use different terms to refer to the same concept. The feature that Rust calls "union" is not a tagged union; it is an untagged union like in C (and exists primarily for the sake of compatibility with C code).
- Values dependent on values (algorithms)
- Values dependent on types (parametric polymorphism)
- Types dependent on types (type-level functions)
This is one dimension weaker than Calculus of Constructions (CoC) [1], which permits types dependent on values (so-called "dependent types"), but we don't need them until we want to prove some properties about our code (dependent types correspond to logical predicates). Some tasks handled by metaprogramming however may not be handled even by a type system that permits type-level functions; for this reason, probably we have to include some sort of simple syntax transformations, such as those found in Idris [2].
I expatiated in one of my posts [3] on why static languages duplicate their language concepts. This writing also includes Zig and Idris.
[1] https://gist.github.com/Hirrolot/89c60f821270059a09c14b940b4...
[2] http://docs.idris-lang.org/en/latest/tutorial/syntax.html
[3] https://hirrolot.github.io/posts/why-static-languages-suffer...