[HN Gopher] Zig-style generics are not well-suited for most lang... ___________________________________________________________________ Zig-style generics are not well-suited for most languages Author : Ar-Curunir Score : 87 points Date : 2022-10-09 16:34 UTC (6 hours ago) (HTM) web link (typesanitizer.com) (TXT) w3m dump (typesanitizer.com) | WalterBright wrote: | 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. | | 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. | mikessoft_gmail wrote: | mo_al_ wrote: | I 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), 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 } | skybrian wrote: | 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.) | wwalexander wrote: | 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. | andrepd wrote: | In 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. | pwdisswordfish9 wrote: | > The past of least resistance | | > getting read of constraints | | Some proofreading is in order. | LAC-Tech wrote: | I'd love language level constraints for comptime types. This + | structs and modules being the exact same thing would be an | incredibly powerful combo. | b3morales wrote: | Well, this answers the question I was curious about the other | day: 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. | nine_k wrote: | Why, closed libs would just have a C interface. | henrydark wrote: | Right, but without generics | throwawaymaths wrote: | 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 | nine_k wrote: | 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... | jmull wrote: | 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. | b3morales wrote: | Sure; here, I'll add a parenthetical "(closed)" to it. :) | jmull wrote: | I'm not sure there will be a lot of call for it, but a code | obfuscator can close source code to the same level | compiling it can. | Kukumber wrote: | 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 | LAC-Tech wrote: | 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. | skavi wrote: | I 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 and C++ has | std::variant. both seem a bit clunky, but usable. | Kukumber wrote: | 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 | | [1] - https://doc.rust-lang.org/reference/items/unions.html | Rusky wrote: | That's a gross misunderstanding of `unsafe`'s place in | Rust, and also of the use cases for `union` vs `enum`. | Kukumber wrote: | I'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 | Cyph0n wrote: | Rust enums are a form of tagged unions. | | Rust unions are plain old unions that are mainly used for | C interop. This is why using them requires unsafe code. | TakeBlaster16 wrote: | 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. | Ar-Curunir wrote: | 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. | WalterBright wrote: | 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.) | Kukumber wrote: | 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 | WalterBright wrote: | Indeed. D used to have complex numbers built in, but the | library type turned out to be more practical. | skavi wrote: | 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. | b3morales wrote: | A Rust "enum" is in reality a tagged union; what features are | you looking for there that are lacking? | Kukumber wrote: | `unsafe` https://doc.rust- | lang.org/reference/items/unions.html | | Therefore it doesn't exist in Rust | vore wrote: | If you need a safe tagged union, you should be using enum: | https://doc.rust-lang.org/reference/items/enumerations.html | | Unsafe unions are for the C-style type-punning unions, | which are intrinsically unsafe. | Kukumber wrote: | Thanks | ameliaquining wrote: | "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). | bmacho wrote: | very 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_ wrote: | > 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#... | Hirrolot wrote: | 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. | | [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... | operator-name wrote: | 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)? | zozbot234 wrote: | 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. | operator-name wrote: | > 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 wrote: | Inheritance is unrelated. C++ has only gone from "duck typed | generics" (the second approach) to statically typed generics | (the first approach). | operator-name wrote: | 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. | programmer_dude wrote: | > 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 wrote: | 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. | oconnor663 wrote: | 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. | programmer_dude wrote: | > 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". | paoda wrote: | 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. | programmer_dude wrote: | > 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 wrote: | 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: // | 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. | hansvm wrote: | > 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. | | fn foo(comptime writerFn: anytype, comptime WriterT: | Writer(writerFn), writer: WriterT) | | 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. | | fn foo(writer: anytype) !void { checkWriterT(@TypeOf(writer)) | } | | 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. ___________________________________________________________________ (page generated 2022-10-09 23:00 UTC)