I find the direction of zig confusing. Is it supposed to be a simple language or a complex one? Low level or high level? This feature is to me a strange mix of high and low level functionality and quite complex.
The io interface looks like OO but violates the Liskov substitution principle. For me, this does not solve the function color problem, but instead hides it. Every function with an IO interface cannot be reasoned about locally because of unexpected interactions with the io parameter input. This is particularly nasty when IO objects are shared across library boundaries. I now need to understand how the library internally manages io if I share that object with my internal code. Code that worked in one context may surprisingly not work in another context. As a library author, how do I handle an io object that doesn't behave as I expect?
Trying to solve this problem at the language level fundamentally feels like a mistake to me because you can't anticipate in advance all of the potential use cases for something as broad as io. That's not to say that this direction shouldn't be explored, but if it were my project, I would separate this into another package that I would not call standard.
i think you are missing that a proper io interface should encapsulate all abstractions that care about asynchrony and patterns thereof. is that possible? we will find out. It's not unreasonable to be skeptical but can you come up with a concrete example?
> As a library author, how do I handle an io object that doesn't behave as I expect
you ship with tests against the four or five default patterns in the stdlib and if anyone wants to do anything substantially crazier to the point that it doesnt work, thats on them, they can submit a PR and you can curbstomp it if you want.
> function coloring
i recommend reading the function coloring article. there are five criteria that describe what make up the function coloring problem, it's not just that there are "more than one class of function calling conventions"
An interface is a library decision, not a language decision. The level of abstraction possible is part of a language decision. GP is saying that this adds "too much" possible abstraction, and therefore qualifies as "too high level". Another benchmark about "too high level" would be that it requires precisely the "guess the internal plumbing" tests that you describe.
Not really advocating anything, just connecting the two a little better.
yes indeed true. but the standard library is in the end just a library, you could reimplement the "pre-io" patterns in the new std if you wanted.
What exactly makes it unpredictable? The functions in the interface have a fairly well defined meaning, take this input, run I/O operation and return results. Some implementation will suspend your code via user-space context switching, some implementation will just directly run the syscall. This is not different than approaches like the virtual thread API in Java, where you use the same APIs for I/O no matter the context. In Python world, before async/await, this was solves in gevent by monkey patching all the I/O functions in the standard library. This interface just abstracts that part out.
I like Zig a lot, but something about this has been bothering me since it was announced. I can't put my finger on why, I honestly don't have a technical reason, but it just feels like the wrong direction to go.
Hopefully I'm wrong and it's wildly successful. Time will tell I guess.
It's funny how this makes the Haskell IO type so clearly valuable. It is inherently async and the RTS makes it Just Work. Ofc there are dragons afoot always but mostly you just program and benefit.
> Every function with an IO interface cannot be reasoned about locally because of unexpected interactions with the io parameter input. This is particularly nasty when IO objects are shared across library boundaries.
Isn't this just as true of any function using io in any other language?
> As a library author, how do I handle an io object that doesn't behave as I expect?
But isn't that the point of having an interface? To specify how the io object can and can't behave.
It's more about allowing a-library-fits-all than forcing it. You don't have to ask for io, you just should, if you are writing a library. You can even do it the Rust way and write different libraries for example for users who want or don't want async if you really want to.
First thought is that IO is just hard
Couldn't the same thing be said about functions that accept allocators?
I start to want a Reader Monad stack for all the stuff I need to thread through all functions.
Yeah, these kinds of "orthogonal" things that you want to set up "on the outside" and then have affect the "inner" code (like allocators, "io" in this case, and maybe also presence/absence of GC, etc.) all seem to cry out for something like Lisp dynamic variables.
A few languages have those, and I don't miss them, because in large codebases it becomes a pain to debug.
xBase, Clipper, Perl, Tcl upvars
It depends on how you do it. XSLT 2.0 had <xsl:tunnel>, where you still had to declare them explicitly as function (well, template) parameters, just with a flag. No explicit control over levels, you just get the most recent one that someone has passed with <xsl:with-param tunnel="yes"> with the matching qualified name.
For something like Zig, it would make sense to go one step further and require them to be declared to be passed, i.e. no "tunneling" through interleaving non-Io functions. But it could still automatically match them e.g. by types, so that any argument of type Io, if marked with some keyword to indicate explicit propagation, would be automatically passed to a call that requires one.
that's basically implicit parameters, the typed, well behaved version of dynamic scoping.
Yep, I believe that's what Scala called that.
And I think we need something like this to get people off globals.
I don't think so because the result of calling an allocator is either you got memory or you don’t, while the IO here will be “it depends”
I don't get it, what's the difference between "got or don't" vs "it depends"?
The allocator’s output is only two: either you get memory or you don’t. To quote GP
> Every function with an IO interface cannot be reasoned about locally because of unexpected interactions with the io parameter input
This means with IO interface is not quiet clear what WILL happen so it “depends”
I thought the same, that zig is too low level to have async implemented in the language. It's experimental and probably going to change
fwiw i thought the previous async based on whole-program analysis and transformation to stackless coroutines was pretty sweet, and similar sorts of features ship in rust and C++ as well
Both of those (C++ and Rust) ship language support for async but not runtimes.