13 ms·
Zig-style generics are not well-suited for most languages
- programmer_dude 4y ago> This post is meant to discuss reasons why Zig-style generics are not well-suited for languages other than Zig. > Limited compiler support when calling generic code > Limited compiler support when writing generic code > Limited type inference > Cannot distribute binary-only libraries with generic interfaces > Tooling needs to do more work > Inability to have non-determinism at compile time > Inability to support polymorphic recursion Why are these things not a problem in Zig?
- lifthrasiir 4y agoThey 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.
- oconnor663 4y agoSpeaking 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.
- programmer_dude 4y ago> 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 https://news.ycombinator.com/item?id=33130533 Edit: I call this misfeature of the C language the "polluted/busy call site syndrome".
- paoda 4y agoThis 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.
- programmer_dude 4y ago> The current build system and type system go a long way to encourage library use I have zero experience with Zig but this contradicts what the other poster in this thread said. Not sure what to make of this.
- paoda 4y agoOh 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: // Argument Parsing Library exe.addPackagePath("clap", "lib/zig-clap/clap.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.
- flohofwoe 4y agoBecause 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.
- skybrian 4y agoI'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.)
- User23 4y agoSoundness 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.
- tialaramex 4y ago> 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.
- b3morales 4y agoWell, this answers the question I was curious about the other day: https://news.ycombinator.com/item?id=33110549 https://news.ycombinator.com/item?id=33110549 > 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.
- jmull 4y agoProprietary 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.
- nine_k 4y agoWhy, closed libs would just have a C interface.
- throwawaymaths 4y agoYou'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
- Kukumber 4y agocomptime 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
- skavi 4y agoI don’t know about the others, but Rust absolutely has tagged unions. It’s just the enum type. edit: just checked the others. D has https://dlang.org/phobos/std_sumtype.html https://dlang.org/phobos/std_sumtype.html and C++ has std::variant. both seem a bit clunky, but usable.
- Kukumber 4y agoLast 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 [1] - https://doc.rust-lang.org/reference/items/unions.html https://doc.rust-lang.org/reference/items/unions.html
- Rusky 4y agoThat's a gross misunderstanding of `unsafe`'s place in Rust, and also of the use cases for `union` vs `enum`.
- Kukumber 4y agoI'm not well versed in Rust, care to educate me to clear misunderstanding? What's the Rust translation of this example?: https://ziglang.org/documentation/master/#Tagged-union https://ziglang.org/documentation/master/#Tagged-union
- wwalexander 4y agoAn 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.
- andrepd 4y agoIn rust the canonical way to get "newtypes" afaiu is to have a 1-member tuple struct, like struct Id(u64); then if `foo` is an `Id`, `foo.0` is the u64.
- wtetzner 4y agoAnd if you want to ensure the underlying representation is preserved, you can use #[repr(transparent)]
- wwalexander 4y agoYep, I remember thinking this was a great approach. Unfortunately, Swift doesn’t allow you to extend “non-nominal types”, which includes tuples. I often find myself thinking of Swift as a not-quite-fully-baked attempt at making a Rust, and this reinforces my impression.
- pjmlp 4y agoOnly 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.
- bmacho 4y agovery off topic, but how does the sites font css work? It is @font-face { font-family: system; font-style: normal; font-weight: 400; src: local("-apple-system"), local("BlinkMacSystemFont"), local("Segoe UI"), local("Roboto"), local("Oxygen-Sans"), local("Ubuntu"), local("Cantarell"), local("Helvetica Neue"), local("sans-serif"); } and gives me the Ubuntu font. Is it the first font in the list, that my browser recognizes and I have?
- kitten_mittens_ 4y ago> Is it the first font in the list, that my browser recognizes and I have? Yes. More info here: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face#description https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face#...
- Hirrolot 4y agoZig 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. [1] https://gist.github.com/Hirrolot/89c60f821270059a09c14b940b454fd6 https://gist.github.com/Hirrolot/89c60f821270059a09c14b940b4... [2] http://docs.idris-lang.org/en/latest/tutorial/syntax.html http://docs.idris-lang.org/en/latest/tutorial/syntax.html [3] https://hirrolot.github.io/posts/why-static-languages-suffer-from-complexity.html https://hirrolot.github.io/posts/why-static-languages-suffer...
- operator-name 4y agoThank 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)?
- Hirrolot 4y agoI'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.
- pwdisswordfish9 4y ago> The past of least resistance > getting read of constraints Some proofreading is in order.
- typesanitizer 4y agoFixed, thanks.
- mikessoft_gmail 4y ago
- operator-name 4y ago> 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.
- IshKebab 4y agoInheritance is unrelated. C++ has only gone from "duck typed generics" (the second approach) to statically typed generics (the first approach).
- operator-name 4y agoIs 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.
- riwsky 4y agoYou’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.
- moonchild 4y ago
- WalterBright 4y agoD 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. https://dlang.org/spec/template.html#template_constraints https://dlang.org/spec/template.html#template_constraints It's simple to implement and understand, as it doesn't introduce any new syntax, and people already know how to write conventional code.
- valenterry 4y agoBut don't forget that it also doesn't compose as well as the other options.
- WalterBright 4y agoI don't see that as true. I don't recall anyone mentioning that they couldn't compose what they wanted.
- arunc 4y agoI don't think it's true. Show some example? Templates work even in betterC mode.
- valenterry 4y agoCan you template a template?
- ben-schaaf 4y agoI believe so; see the entire meta package in the standard library: https://dlang.org/phobos/std_meta.html https://dlang.org/phobos/std_meta.html
- valenterry 4y agoUsually 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.
- mo_al_ 4y agoI think that Zig will eventually add a way to constrain types. A note about C++. Prior to C++20 concepts, you could always add constraints to templates via [SFINAE](https://en.cppreference.com/w/cpp/language/sfinae https://en.cppreference.com/w/cpp/language/sfinae), it had the tendency to be verbose: template<typename T, typename = enable_if_t<is_integral_v<T>>> T add_2(T x) { return x + 2; } struct Writer { virtual void write(const char *) = 0; }; struct SubWriter: public Writer { virtual void write(const char *s) override { puts(s); } }; template<typename T, typename = enable_if_t<is_base_of_v<Writer, T>>> void write_to(T x, const char *msg) { x.write(msg); } int main() { printf("%d\n", add_2(4)); // works write_to(SubWriter{}, "Hello"); // works } With C++20 concepts: integral auto add_2(integral auto x) { return x + 2; } struct Writer { virtual void write(const char *) = 0; }; template<typename T> concept Writable1 = is_base_of_v<Writer, T>; // uses type_traits template<typename T> concept Writable2 = requires (T t) { // uses compile time requirements, similar to Go interfaces { t.write("Hello") }; }; struct SubWriter1: public Writer { // satisfies Writable1 and Writable2 virtual void write(const char *s) override { puts(s); } }; struct SubWriter2 { // satisfies only Writable2 void write(const char *s) { puts(s); } }; // accepts both SubWriter1 and SubWriter2 void write_to(Writable2 auto x, const char \*msg) { x.write(msg); } int main() { printf("%d\n", add_2(4)); // works write_to(SubWriter1{}, "Hello"); // works }
- LAC-Tech 4y agoI'd love language level constraints for comptime types. This + structs and modules being the exact same thing would be an incredibly powerful combo.
- pron 4y ago> 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.
- haberman 4y agoI 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.
- akkartik 4y agoI don't know what you mean by #1. Do you mean the syntax of putting `comptime` args into the arg list along with the regular ones?
- haberman 4y agoBy #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/#dynamic-specialization https://ikrima.dev/dev-notes/zig/zig-metaprogramming/#dynami...
- varajelle 4y agoFrom c++17, You can also use a if statement in c++ (if constexpr (std::is_same_v<T, int>) {...})
- haberman 4y agoYou 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".
- sivizius 4y agoIssue 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.
- olodus 4y agoReally 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.
- HelloNurse 4y agoI'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.
- Tozen 4y agoAgree. Nothing embraced by a particular language, needs to be well suited for most other languages. There will always be differences and preferences.