Hacker News new | past | comments | ask | show | jobs | submit login
Easy Interfaces with Zig 0.10 (zig.news)
110 points by todsacerdoti on Nov 17, 2022 | hide | past | favorite | 71 comments



To give some context: this article is meant for people who program in Zig who might be interested in the new `inline else` switch case feature, and the metaprogramming it enables.

If you're looking for more general purpose interfaces (eg vtables) than what tagged unions can provide, then other material will be of more interest to you:

https://zig.news/david_vanderson/interfaces-in-zig-o1c

https://zig.news/david_vanderson/faster-interface-style-2b12

https://www.youtube.com/watch?v=AHc4x1uXBQE


I think this article conflates interface and specification:

interface: describe required inputs in exchange for perceived outputs. No more, no less. Environmental concerns or invariants are the responsibility of the conformer. Interfaces communicate a boundary of incomplete information while allowing interoperability in face of incomplete information. Interfaces are explicitly inclusive.

specification: describe the exact inputs and exact outputs while describing all invariants. specifications communicate the exact conditions in which inputs/outputs can be exchanged and all possible (known) invariants. Specifications are explicitly exclusive.

Per the article:

  const Animal = union(enum){
     cat: Cat,
     dog: Dog,

Is a specification not an interface. At no point can I conform to this "interface" without the "interface" knowing of my existence. Animal must know about all possible invarients to operate correctly, such that it is a specification.

If I want to join two pieces of pipe together, the interface would be the inner diameter say 1". The specification of some use case would say only 1" inner diameter pipes can be joined with other 1" inner diameter pipes that have an outer-diameter of at minimum 1.25" due to "pressure constraints". If I choose a pipe that has the same inner diameter but different outer diameter, I've conformed the interface but failed the specification.

The rest of the article seems to try and affirm the interface argument by jumping into syntax and compilation arguments. I fail to see how this has anything to do with Zig or abstract ideas of interfaces.


> The rest of the article seems to try and affirm the interface argument by jumping into syntax and compilation arguments.

For the purpose of that post I'm loosely defining interfaces as "runtime polymorphism". The article "jumps" into syntax because that's what I'm trying to show, there really is no argument that I'm making about the abstract idea of an interface.

As stated at the top of the article (and in other comments here), if you want vtable style interfaces, you can have vtable style interfaces: just make a vtable and use it, the language won't stop you :^)

> I fail to see how this has anything to do with Zig or abstract ideas of interfaces.

Yep, that's because the article was never about abstract ideas of interfaces. It does pertain Zig specifically because it shows off a new language feature: `inline else`.


You're right about the difference, but I very much doubt the term "specification" as you use it is common.

It's just a different approach to dealing with the expression problem using sum types.


Sure, use vtables if you need vtables.


Aw, I get that this is just showing off use cases for the new inline else feature, but from the title I was hoping it had more to do with this issue[0]. Having only used Zig enough for an Advent of Code, interfaces via a mechanism akin to rust's traits is probably my highest wishlist feature.

If I understand the post here correctly, the version of interfaces shown affects the runtime data representation. And tagged unions have to be at least the size of the largest variant, sometimes plus the tag (right? Or is that just true in rust?).

Ideally, to me, an "interface" can be used as purely a compile time constraint. With rust traits, to use the example in the post, when you define a function that takes an "Animal", it will basically generate a version of the function that takes a Cat, and one that takes a Dog, and double checks that Cat and Dog both have `talk` implementations. So the tradeoff is you get static dispatch with no wasted space in the data representation, at a cost of more code generation.

Zig's comptime is powerful enough, that you can maybe even implement this version of traits in "userland" code, and that linked issue has some neat examples, though I think it would be better in the compiler itself so it can more easily be used throughout the ecosystem and so it interacts better with tooling (e.g. doc generation).

[0] https://github.com/ziglang/zig/issues/1268


> And tagged unions have to be at least the size of the largest variant, sometimes plus the tag (right? Or is that just true in rust?).

That's correct. If you have wildly different sizes, you can create a union of pointers. It will require you to manage the memory for the concrete implementation separately, but this way you will not waste space.

    const Foo = union(enum) {
       dog: *Dog,
       cat: *Cat,
       // ...
    };
> With rust traits, to use the example in the post, when you define a function that takes an "Animal", it will basically generate a version of the function that takes a Cat, and one that takes a Dog, and double checks that Cat and Dog both have `talk` implementations. So the tradeoff is you get static dispatch with no wasted space in the data representation, at a cost of more code generation.

Yes, that post is specifically about the need for dynamic dispatch, in Zig you can get monomorphization like so:

    fn makeAnimalTalk(animal: anytype) void {
       // optionally use some metaprogramming to validate `animal`
       // and produce a custom compile error when the trait is not 
       // fulfilled.

       animal.talk(); // will work on any type that can be used like that
    }


Is it still a feature of the language that it's rules are simple enough so you don't need to debug your knowledge of the language?

Neither this `inline case else` nor the async/await make sense to me. Granted, I don't use the language, but when I tried to read the documentation/tutorials to understand them, I couldn't. With async/await, I read the docs then tried to debug my understanding and the small amount of code I wrote didn't work the way I expected.


This inline else example makes sense when you stop and think what it is doing, but it is in no way intuitive.

Taken at face value, why is it switching on self with no case whatsoever? Why does the else case need to be inlined? This is trying to be smarter than it needs to be.

Meh, not a fan.


The fact that you ask why it needs to be inlined means you haven't understood how the feature works. If you did, you'd be a fan. It's a general mechansim that fits Zig perfectly, along with inline for and while.


I understood why. I said it is very non-obvious and opaque at first read, so it might trip up a lot of newcomers.

Surely there's better ways to express interfaces than exploiting a brilliant side-effect of compile time inlining.


don't know about the language rules, but I've had to wait for the explanation to understand what was about to happen. Naming is hard, d'oh, but I feel this could have a better name. "inline" does not trigger my brain in a way a "comptime_expanded" or "comptime_generator" would, e.g.

I believe andrew and loris have been with zig for long enough to have their own bias and no longer be aware of it, thus I believe it's the newcomers' job to hold up the simplicity design premise now...


"inline" constructs all behave kinda the same way in Zig, like I've explained in the article. Once you're used `inline for` etc, it all works in a fairly homogeneous way.

This article was mostly about pointing out how `inline else` lends itself really well to writing tagged union interfaces, I would expect anybody who actively writes Zig and has metaprogramming experience to not have much trouble using it.


That's what I mean with you having developed a bias and no longer being aware of it [no insult intended] ;) If you know the concept in that language and its name-tag, then it's no longer confusing, just as lisp macros aren't anymore once you grokked them. As a language in development, I would hope the on-boarding process from people with a different background would be designed to be as smooth as possible. IMO, someone coming from a different, popular systems programming language will probably have quite a different expectation what "inline" means - I know I do. Maybe finding an alias/new name for "inline" here can be considered before a "1.0".


I don't think this is Zig-specific terminology, though. For example, F# has something similar for functions:

https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...


Sure, I don't mean to claim that what inline does is self-evident. This article was really meant for people in the Zig community and in some sense it's unfortunate it reached the front page of HN.

> Maybe finding an alias/new name for "inline" here can be considered before a "1.0".

I would be on board with that, yes.


The Zig syntax has been drifting for some time and it is not in a good place.

I am afraid that the designers are blind to it because they are using it every day.

I am also afraid that once a lot of code and libraries are written, it will be difficult / impossible to change the syntax, it might already be too late.

I love Zig but the syntax quirks might considerably slow its adoption.


Why isn’t the compiler just doing inline whenever possible? Why is it a user-facing feature of the language, it looks like it should be a compiler optimization to me.


Inlining (in the C/C++ sense of the term) is not always preferable.

On top of that, in this specific case, `inline` has a Zig-specific meaning that pertains to generics and metaprogramming in a way that has not much to do with the C/C++ use of the term.


The programmer might know more than the compiler about which things should be expanded at compile time and which things should not. Zig is intended to be able to be used to generate optimal code.


But "inline" is routinely used in C in "inline functions".. I don't think anyone with a decent amount of experience with C would have trouble quickly seeing what inline means in Zig, and those people are the primary target audience for Zig.

It's short and concise IMO.


I have a lot of experience with C/C++ and I don't understand what this really has to do with inline expansion or linkage behaviour.


Makes enough sense to me, although I was using Zig years ago.

An inlined function is a function that is not called in the traditional sense and instead its effects are replicated as though the function code had been written at the call site.

With `inline for` and `inline while`, the for loop isn't run in the traditional sense and instead is unrolled as though each instance of the loop had been written separately.

With `inline else`, every possible instance not covered by a `case` is expanded as though it had been written separately.

It's a little weird to C programmers only because C doesn't expand the concept outside of functions.


Inline has a different meaning in C/C++, and to be honest it was not a very good keyword choice in the first place.


> I believe andrew and loris have been with zig for long enough to have their own bias and no longer be aware of it, thus I believe it's the newcomers' job to hold up the simplicity design premise now...

Zig aside, that's an astute observation in general. I think this is an issue in Python where the old guard that have been part of the project for decades and are extreme experts have gotten bored and started cramming in new language features.


It's a smallish language and a big part of the appeal to me is that I can fit most of it in my head.

Async/await is a sharp corner, easier to start elsewhere.


The funny thing is async/await is actually fairly easy to understand (IMO a bit finicky to use, partly because manually managing resources with that control flow is tricky, partly because of bugs that need workarounds pre-1.0), but the docs aren't stellar, so you have to experiment a bit or ask the community how certain code is supposed to be have.

As one example, here's a description of "suspend" straight from the docs:

> At any point, a function may suspend itself. This causes control flow to return to the callsite (in the case of the first suspension), or resumer (in the case of subsequent suspensions).

That description isn't quite right though. Control flow returns up the call-stack to the most recently executed async-aware bit of code. In particular, that'll either be the most recent _async_ callsite or whoever last resumed this function. It's precisely those semantics which enable one to write a nice event loop API, but the docs tell you something different.

Saying that aloud, I should probably just file a PR and fix that. Off to $WORK soon, but that sounds like a good this-evening problem if nobody grabs it first.


The 'inline switch else' seems easy, no? "inline switch" should be self-explanatory. It expands at compile time like inline for loops or inline functions. It is somewhat unique that the switch case gives you a reference to what you matched with |x|, but isn't it essentially just shorthand for self.x? And that |x| is necessary when you want to do self.x.talk() where you don't know x

I can agree that async/await is tricky to understand properly. I managed to use it to write some nice code for a demo of a Verilog simulator, but it took a few tries to get it right, because async/await/suspend/resume definitely isn't intuitive to start with. Once you understand it the rules are quite simple though. I'm sure a well written tutorial could make most people understand it.

And it depends what you're trying to do. Using suspend and resume correctly can be hard. But most of the time that will be done by people writing libraries, and when you have an async aware library and an event loop, using just async and await isn't that hard.

Zig is a new language that isn't really finished. Documentation is sparse and they're still making breaking changes. To me, Zig is a very simple and concise language, much simpler than C in many ways (we're just used to C's quirkiness), and I'm sure when it's stable and there's better documentation and tutorials it'll be easy for most people to get a thorough understanding of it quickly.


> but isn't it essentially just shorthand for self.x

At least in the `else` prong of the switch, if you want to do anything with the value you have to capture it, because `self` will be the union type, not the particular type within the union.

I've previously asked about whether this could be done away with, like if you write `if (x != null)`, could that create a scope in which x refers to the non-null value in rvalue-ish contexts but to the nullable value if you want to set it to null? Some compilers like Sorbet do this and call the feature "type narrowing." It seems like it is a bit too magical to make it into Zig though.


The feature is nice.

The syntax is not.

Using "inline else" is both weird, confusing and incorrect.

It is allowed to create new keywords for new language features.

And please don't use special symbols, this should be kept to the strict minimum, in my opinion Zig is already using too many symbols.

The use of the static keyword in C/C++ is an example of something that should be avoided.


The problem here is that all variants (Cat, Dog, Snake) have to be known when defining the interface (Animal). That makes it not extensible. I prefer Rust's traits.


well, that's an innate feature of comptime expansion (or "macro" if you will) and as such this doesn't inhabit the same solution space as dynamic-dispatch interfaces. Benefit of this is it's boiling down to a statically known jump table and thus can be optimized accordingly, ideally boiling down to a direct inline in respective caller paths. The cost for this is whole-program compilation each time. It's a different trade-off, for sure.


Rust traits do not require dynamic dispatch nor a jump table. Therefore rust traits are inherently faster than zig unions.

They can be used via dynamic dispatch but this is not the default.


You just wouldn't use a union if you wanted monomorphization and didn't want e.g. a heterogeneous collection. Even when it is possible, "monomorphize everything" isn't always faster though, because for example ITLBs are not infinitely large.


Yes, it does seem like something the compiler should automatically define for you.


I have not touched zig, yet. But I hope they will use the duck typing approach how Golang does it, which makes it really nice to wrap APIs, mock APIs without explicitly mentioning an interface


Go's interfaces are an example of structural subtyping, not duck typing. The difference is that the structure a function expects is defined at compile time (i.e. in the function's signature) and not dependent of runtime usage of the passed in object.


It's also quite frustrating in practice. It's often hard to understand that an error is actually lack of conformance to an interface (and why) and even worse, it's not uncommon to accidentally implement an interface you didn't intend to.


I haven't touched zig, either, but I hope they don't go with structural typing. It leads to bloat: defining an interface explicitly to support unit testing, when stronger typing and appropriate use of fixtures and integration testing are much a much better approach.


I guess it is the combination of duck typing + struct embedding + first class functions which let’s you adapt to an api or provide an api in a natural way without jumping through hoops


I hope nothing is done similarly to golang, but you can do this by writing `anytype` which you may know as `auto`


> If you had that reaction, you might want to take some time to explicitly free your thought process from OOP-isms.

If you have this reaction, perhaps you should take a humility lesson before claiming that a few syntax tricks is all it takes to outsmart people who spent their entire university careers thinking about these problems.


> before claiming that a few syntax tricks is all it takes to outsmart people who spent their entire university careers thinking about these problems

I don't think I have written anything of that sort. Thinking in OOP terms in, say, Java is great. Zig is not an OOP language though, and it cares about mapping nicely to CPU instructions more than it cares about mapping nicely to UML diagrams, so thinking in OOP terms in Zig is not that great, more often than not.

I've seen plenty of people coming into Zig expecting it to conform to their expectations towards object orientation or functional purity. Neither will be able to have a good time if they can't let go of their preconceptions.


Sorry for misunderstanding the mood and context. In context of Zig, this makes a lot of sense, making my calls for humility premature and tad ironic.


Nah, OOP is bad. It's been a while since I met anyone, online or offline, who still takes OOP seriously.


/s, right?


Poe's Law strikes again!


No.


Well, you've met one now, so you can update that assumption. And also if you look higher up this thread, the author of this article takes it seriously as well. So that's two already in a pretty short time!


The comment you’re referring to merely refrains from making a value judgment, while you’re turning it into an approval of OOP.


>Thinking in OOP terms in, say, Java is great.

How did you interpret this statement?


Compare:

thinking in OOP terms in say, Haskell, is terrible. -> doesn’t mean OOP is terrible or that Haskell is terrible.

Thinking in functional terms in Haskell is great. -> doesn’t mean functional programming is great or that Haskell is great.

All it means is that the language is designed to be used with that paradigm.


People spend entire careers thinking about object-oriented programming?



Heh. I'm sure they thought and worked on others things too, but many focused on it, yes.


Any recent innovations? Because even traditional object-oriented languages like Java and C# seem to be getting more functional over time, and the endless attempts to add classes to Javascript only make it worse.


Lots of them, people only have to look beyond Java and C#.

Many of those "functional" features like LINQ, were already present in Smalltalk collections and CLU.

As for what inovations, OOPSLA, ECOOP, ACM, IEEE proceedings have plenty of research papers on the matter.


Not quite, people think in Inheritance instead.


A lot of people have spent their entire industry careers dealing with these problems, and the result seems to be the sentiment you've encountered in this comment section.


I want to like Zig but after trying a couple programs it has a few annoying nuances. I believe you can't use tabs which is overly opinionated and the odd print requirement to use empty brackets is overly verbose.


Something I found eyebrow-raising is that the expected type of expression `x` in `if (x) { ... }` actually changes based on what syntax is used on an `else` line that comes later.


I haven't seen that yet. Could you give an example?


IIRC in the following, the expected type of `x` in B stops being bool (but I no longer have a Zig env set up so I can't test it).

    # A
    if (x) {
        ...
    } else {
        ...
    }

    # B
    if (x) {
        ...
    } else |y| {
        ...
    }


That looks right. If/else is overloaded as doing the normal thing with bools by default, and if you try to capture something exceptional with `else |y|` then the type of `x` is inferred to be something capable of such a capture -- optionals or error unions.


In a statically typed language, this specifically comes off to me as an artificial way to keep the total number of keywords down.


I think any text editor can convert tabs to spaces. I've never had this issue for any number of languages


Zig 0.10.0 allows tabs. I think 0.9.1 also allowed, but I'm not 100% sure.


When I use tabs I get "./fizzbuzz.zig:4:1: error: invalid character: '\t'"


And all the dots.


Does anyone have a real-world example of where this is applied?

Nice to see a new feature that reduces boilerplate, buy I’d rather see a real case where it was useful rather than an artificial one


> Does anyone have a real-world example of where this is applied?

When you want to make an array of different structs that can be used in a homogeneous way. For example I made a Redis client library that has a bunch of commands implemented as structs (GET, SET, etc). You can't just put them into an array directly because each is its own type, but they all support being sent to Redis when passed to a client instance.

Putting those commands into a tagged union helps with the creation of things like interactive CLI clients.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: