[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)