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