News for hackers. Stuff that matters.

Twey

> How would the future get the I/O result then?

You can write directly into the future (if you own it and know its structure), use a channel, etc. It's not the best ergonomics (see the comment uptree about this) but it's doable.

> This makes no sense, why would I ever want to do that?

Why would you ever want to wait on a Waker that you won't wake? :)

> That's a very big if. I said "easily" and this is not easy at all.

> Moreover it ignores the context: the SansIO/async/await code is generally a library written by a user, while the code that wants to use it is written by someone else entirely. Good luck sharing this external notion between the two!

It's not more difficult than with the manual state machine? That also has no general notion of inputs — you have to call specific functions to advance particular states with their inputs. There's no general equivalent of `poll`; the library and its caller have to agree on an API.

> I agree that it would be ideal to have a way to automatically generate the state machine from straightforward looking code, and async/await kinda does this, but this usecase requires more flexibility than what async/await provides.

I'm not trying to claim that (Rust's particular flavour of) async/await is suitable here. It certainly has some sharp edges, and it may well be that some of them bite for this project. I'd have loved to see an analysis of what and how, if so.

My point is just that use of async/await is not excluded by wanting to write some code ‘sans-I/O’. That's not a claim that it's universally applicable to all problems.

> An generic future is much harder to be called like this. You need to to create wakers and if you don't properly handle them you'll busy loop. You also need to provide the exact runtime the future needs for its I/O.

You don't actually have to handle I/O with the async runtime if you don't want. You _could_ just poll the futures yourself in a loop, which is roughly the same as what happens if you just call the transition function in a loop for a manual state machine. And likewise if you just keep calling the transition function with empty input you'll also busy-loop.

> And isn't that what blocking code does all the time?

Sure, if you have only one flow you care about or you have threads to take care of your concurrency, this is fine. You can do it with any model (blocking, async, manual state machine).

> - you can easily use it from blocking code without needing a runtime - you can easily use it from async code without blocking the executor

But these things are already true of a state machine compiled from async/await instead of written by hand. `poll()` is just a function, and you can call it where you like. By the contract of `Future`, just like the contract of the state machine transition, it's guaranteed to ‘not block’. The waker machinery exists to help you write engines that decide when to poll, but if you've got your own external notion of that you're free to just call `poll()`. The standard library even has some optimization around this case, e.g. `Waker::noop()`.

> You can for sure describe this as "pure" and "functional", but I bet most people think of a different paradigm when talking about pure functional programming.

I agree, but it's confusing to me that this is the case. Original pure functional programming was all about ‘how can we write more useful programs without any effects’ and ‘what benefits can we get from doing so’. I think those questions are kind of answered; since Moggi's work in the early nineties it's been focused more on ‘how can we keep the benefits of pure functions while also being able to encode effectful logic?’, to which manually-written state machines are a rather primitive sort of answer. They bear pretty strong resemblance to iteratees/transducers/pipes/etc. from that lineage, except that those things aim to build a higher-level combinator language on top to reduce the amount of manual state-machine writing one has to do.

Message-passing systems have always had the capability to deadlock: if you have two processes that each wait for a message from the other, you have deadlock, just the same as two threads trying to take a lock. So long as the memory is either read-only or else transferred, not shared (the shared ^ writeable rule) you don't get hit with race conditions, which are the problem with shared memory.
The programming model and the implementation are not 1:1. This thread is mostly about the model; we can talk about implementation below :)
Message-passing doesn't necessarily involve naïvely copying the data around. If you have access to shared memory then you can pass a pointer; the only thing you really need to ‘pass’ is the capability to access the message data. This is how seL4 does it, for example.

Of course if you don't have shared memory then you have to copy anyway :) And because of CPU caching architecture the benefits of having an owned copy of the data can sometimes outweigh the cost of copying it, even if shared memory is available.

I think at the end of this long road lies algebraic effects. You can write your computations with typed effects and, once you've handled every effect (or all but a finite subset, if you want to leave some to the runtime), you can drive your computation generically. Because the effects are typed, you get to narrow the types of the effect arguments at each yield, so you don't have to handle every possible effect at every yield (on either side of the channel).
> What if I want to change some state in-between two state transitions (i.e. between two `await` points)?

I think as phrased what you said is an _advantage_ of the Rust model: the state of your state machine is entirely contained within the dedicated state-machine type and you know its behaviour can't depend on surprise outside state.

But I think the spirit of your criticism is correct: you do really want to be able to type processes with intermediate inputs and outputs, in general, and Rust makes this hard (though not impossible: see Munksgaard's work on session types in Rust, for example). I would not have had a problem if the original article had decided that Rust's futures are unfit for its purposes, and it would have been an interesting case study in what exactly real-world code needs from the API; my complaint is just that they get categorized as a type of I/O, which couldn't be further from the truth.

Message-passing is popular in HPC (e.g. MPI) because it more closely models how hardware works. But at a higher level people mostly prefer to build abstractions on top of it like async/await because message-passing requires you to manually compile your flows into per-process state machines, and that's not how most people think about flows (even though business people do sometimes: https://link.springer.com/chapter/10.1007/978-3-540-72863-4_...).
Your comment is phrased as a rebuttal, but I actually agree with it wholeheartedly, with one minor quibble.

> Blocking threads are a much more natural way of thinking about concurrency.

The (ideal) programming model for writing a thread vs an async task is identical, and indeed the most natural way of writing sequential flows that may incidentally be temporally separated. A blocking operation and an await are equivalent (in the absence of shared memory; in its presence they differ in that an async task on a single thread gets to assume atomicity of the code between awaits, but that's not too relevant here). I think what Joe is arguing against here is exactly splitting up your business flows into a bunch of explicit state machine transitions.

The difference is only in the caller. Callers of async tasks (à la Rust) have the right and responsibility of controlling how (and whether) the task they create resumes, while threads/BEAM processes (and ‘stackful’ futures à la Go, JavaScript, etc.) are answerable only to the great global event loop in the sky (sometimes known as the OS scheduler or the BEAM multiplexer, etc.). This makes it easier to avoid bugs that are about forgetting to drive a task, but harder to avoid bugs that are about forgetting to stop a task, or stopping a task wrongly; see structured concurrency. It also hurts composability a bit, because by construction there can be only one event loop and there's no way to decompose it.

You can see Erlang (or I suppose we should talk about BEAM more generally now people like Elixir) in terms of OS processes as an analogy, but it's just as valid and perhaps a little more accurate to think about it as a pool of async tasks with a global event loop, à la Go, where all blocking-looking code is actually async. It shares a common problem of such languages that because it can't interrupt real blocking code, e.g. FFI, you have to be very careful not to accidentally starve your other processes with such a call. This is made more obvious (but not really fixed) by languages that colour async functions differently, because the async colour acts as a promise (sadly not enforced by the language) that the function will be a good neighbour and not block for too long (whatever that means).

> Function coloring is about (the annoyance when) composing interfaces designed around those different runtimes/thread that might use the same sans-io library internally.

No, it's about the difficulty of calling async functions (or functions with other typed effects that can't be handled locally) from pure functions (or more generally functions that aren't themselves typed with that effect).

> Yes async compiles down to a statemachine, but we're talking about the elegance of the language syntax/composition. Most languages require _a lot_ of ritual (or overhead) if you want to use the async-statemachine as a pure statemachines and sidestep the build in runtime-side-effect channels.

That's true, which I would consider a limitation of those languages. Thankfully Rust futures are exactly state machines; there are some rough edges on their API still, but I'd have preferred a discussion about those to throwing the baby out with the bathwater and claiming that those state machines are a type of I/O.

> This is incorrect. You can easily call that flow from a blocking function, just call it in a loop.

Sure, the same can be said of an async library — so long as you're okay with none of your other flows being able to make progress in the meantime. You can just poll in a loop.

> It's the best of both world but as always you have a tradeoff, and in this case it's the ability to express the state machine as if it was a straightforward function.

‘The best of both worlds’ means that it shares the advantages of both, but the main (arguably only) advantage of async/await is that you get to write your temporally separated flows sequentially. (And maybe also that they look a bit like functions, I guess.) That's not true here.

> A Sans-IO library needs none of these.

To be clear I'm not by any means arguing that ‘sans-IO’ (or, as we used to call it, pure functional programming) is a bad thing. I'm very in favour of it. I'm just saying that a ‘sans-IO’ design shouldn't need to eschew async/await, which you can write in terms of pure functions.

(As a sibling comment points out, there are languages where that is not true, i.e. async/await inherently introduces some kind of side effect around concurrency or a global event loop. Thankfully that's not the case in Rust.)

It was all going great until I read

> The design also avoids problems like function coloring and plays well with Rust's ownership rules.

Async is not I/O. It can be used for I/O, but it's just a way of writing pure, deterministic state machines that keeps a logical flow together instead of scattered over a bunch of transitions.

Even if you choose to manually compile your async flows into explicit state machines, you still have to deal with ‘function colouring’ (a.k.a. typing): a flow that crosses several state transitions (and may have other disjoint parts of the state updated in the meantime, via other transitions) now has to be rewritten as a (series of) state transitions, and can't be called from a blocking function. If anything, rewriting all your state machines without `async` makes the function colouring ‘problem’ worse, because multi-transition flows are even more different from normal functions.

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @01:55PM (#50234132)
I think that used to be true when systems were simpler and expectations were lower. But the expected standard of modern languages has come a long way. And I'm not just talking about libraries: performance, safety (strong static type systems are one can of worms, GC and other runtime checks are another), and even syntax design are things that state-of-the-art languages spend years iterating on before they're considered ‘usable’.

Some other more recent data points:

Go was designed at Google (one of the biggest software development entities of our times, with input from existing PL luminaries) in 2007, released in 2009, and officially considered 1.0 in 2012, five years later;

Clojure was started in 2005 (there were two other attempts to build similar languages dating back to 2003, but let's charitably omit them) and hit 1.0 in 2009;

Zig has been going for about 11 years and hasn't hit 1.0 yet.

Rust's nine-year record (2006–2015) is on the longer side, but not a huge outlier.

Of course, this is just the time to 1.0; I think in practice languages tending to iterate a lot before becoming mainstream (which is a weak proxy for general usability, but at least should work in one direction). Rust today has come a long way from 1.0, for example, as has Go. Definitely the ecosystems at 1.0 releases tend to remain noticeably underpowered, and even if you want to ignore libraries for a language it's harder to justify excluding other tools that make it easier to work with the language on your own or with other people to build real-world software, like editor support, debuggers, documentation, or package managers.

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @11:46AM (#50232820)
Famously, as with many other things in computer science, writing a toy programming language that can compute Fibonacci is doable in a couple of hours, but writing a usable programming language that needs to stand up to real programs for users is typically somewhere between a decade and a lifetime of work.

If he'd announced the language twelve years ago and then not touched it again until a few months ago I'd believe you. But there's plenty of documented evidence that he's been spending a bunch of time on it, and (allegedly) even using it in his commercial projects.

Not to say that he's been working on it full-time for 12 years, of course, but even allowing for part-time work that's far beyond the initial ‘easier than The Witness’ premise.

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @11:41AM (#50232756)
> that is probably 10% of a PL if you want it to do something people will actually use

I think this is really understating the challenge of generating machine code that is competitive in terms of size, performance, etc. with state-of-the-art compilers. I'll be the first to agree that it's far from the only (or even the biggest) difficulty in PL, but it remains a huge chunk of engineering work. The interesting part of this experiment is how much of that work, as a percentage of the work of building a programming language, is now given to you for free, for a language that is indeed mostly ‘just a syntax wrapper over C-like functionality’.

(Of course, a big part of ‘people will actually use’ is nothing to do with the language at all, but the next twenty years of building an ecosystem and community buy-in.)

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @06:14AM (#50229965)
I'd agree more except that as far as I know (I'll admit I've not been following the development that closely) he's been significantly building it in public on Twitch. So he's happy to spend time sharing it with the public, and has been from the start; it's just taken a long time to get it to a usable state.

> If you are fine with getting there 80% his claim is correct. That he's using the language for his games proves it to me. Of course, if you require it to be both open and popular to be a success you kind of reach a stalemate. In a world where everyone can quickly whip up a language I would expect most to be closed to the public anyway.

I think we're firmly in agreement here, except that IMO the default for software that's not for public consumption is to throw it over the wall, put the source up on GitHub or something, and not bother with documentation, explanation, or interaction. A carefully curated closed beta is kind of the opposite of that. If anything he's spending more time promoting it than I would expect.

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @05:56AM (#50229856)
In fact reading between the lines of that video a bit, I think he might believe that general-purpose languages are poorly suited to his use because they're focused on solving other people's problems. But that's just me extrapolating.
Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @05:49AM (#50229817)
LLVM doesn't have to be great for its existence to make PL dev a lot easier. Codegen used to be a real pain.

The interesting part to me was always that he didn't justify Jai very much. He didn't assert that anybody else would want to use it, or that it would be universally better than C++. His claim was that he didn't need to: building a language is now so quick that he can make a language just for him, just because he feels like it. Everybody can have their own language frontend.

https://youtu.be/TH9VCN6UkyQ

Re:Rewriting a Commercial Engine in Jai on Sunday October 25, 2026 @05:27AM (#50229705)
JB's original claim, at least as I understood it, was that with LLVM and the like language tooling is now good enough that anybody can quickly whip up a language to their personal tastes with no PL background, and therefore the hurdle of novelty and generality required for a language to justify its existence is drastically lower than it has been in times past. It's a shame, but having his personal language be in closed beta twelve years later is pretty strong evidence against that thesis in a couple of different ways.
Re:What Is Gridifcation on Thursday October 22, 2026 @08:52AM (#50194665)
Looks like a standard editing mistake to me. The previous sentence probably used to say something like ‘musicians used to not be able to play together’ but when it was edited the editor forgot to update the next sentence for coherence. Editing long-form text is hard :)
Re:Differences Between `Foldl` and `Foldr` on Monday October 05, 2026 @06:30PM (#49971718)
In other words, `foldr` (flipped about) takes a list to its Church encoding: since it just ‘replaces the constructors’ (`:` becomes `f`, `[]` becomes `z`), it doesn't lose any information from the list: if you can write a function by recursing on a list then you can write it using `foldr`.
Re:Differences Between `Foldl` and `Foldr` on Monday October 05, 2026 @06:26PM (#49971683)
Well, `foldr` is (up to interdefinability) just the implementation of `for_` (or `traverse`) in the case of lists, in the same way that `map` is the implementation of `fmap`. The point of `traverse` is that it's provided by the functor.

The question of whether to use the most generic (for consistency) or the least generic (for directness) name for a function is the old bugbear of the Prelude :)

Re:To Grieve, or Not to Grieve? on Saturday October 03, 2026 @10:26AM (#49944566)
And now we have machines to almost perfectly capture an image of any scene, why do we keep paying painters?
Re:What About Rails? on Saturday September 26, 2026 @10:18AM (#49856820)
I think LLM chatbots are great for discovery. Better than CLIs, better than WIMPs. Where previously you'd go to a search engine to figure out what command to run or where to find that obscure tickbox, now the search is built right into the interface, and you can give it arbitrarily complex queries in natural language. It turns out there are a huge number of products for which discovery is the bottleneck.

The problem with chatbots is that they don't scale very well with you. A proficient user of your application knows where that tickbox is and can get to it in three clicks (or if you have shortcuts, one keyboard chord). But the chatbot still makes you type a whole (ish) sentence. If that's something you want to do 3 times a minute that application is now unusable.

Re:What About Rails? on Friday September 25, 2026 @10:56AM (#49845580)
For sure! One of the big draws of malleability is that you can adapt it to your particular needs. Natural language is a UI just as much as a WIMP GUI, and for some tasks or some people it's a great one.
Re:What About Rails? on Friday September 25, 2026 @09:38AM (#49844503)
Yes you caught me out in a bit of exaggeration there. For the topmost level of tasks people typically do want a low-latency, high-bandwidth UI for displaying and manipulating the information they care about.
Re:What About Rails? on Friday September 25, 2026 @09:36AM (#49844467)
In case it wasn't clear, that was intended as a somewhat satirical representation of the position I was discussing :) An important part of any ‘universal UI’ — and arguably the hardest part of building it — has to be that it allows feedback from the API about what and how to display, which is always in tension with the malleability/universality aspect. People are trying to solve that for chatbots right now, with limited success so far.
Re:What About Rails? on Friday September 25, 2026 @09:33AM (#49844418)
I think the popularity of these chat interfaces disagrees with that to some extent. We do already have malleable software for people who can program — the problem has always been bringing it to everyone else.
Re:What About Rails? on Friday September 25, 2026 @09:30AM (#49844382)
As a developer/user I hope for that outcome too — if everybody is using your app through an agent you might as well cut the maintenance cost and drop the GUI, or at least have a nice API alongside it to make it nicer for the agent. But I wonder what it does to the financial incentives to produce software.
Re:What About Rails? on Friday September 25, 2026 @05:23AM (#49842066)
> 37signals differentiates their products with opinionated UI/UX, not novel features. They are rewriting Hey as six native apps because the web fidelity isn’t good enough. So UI matters enough to justify complete rewrites, but also everyone just wants CLIs?

I've seen this one a few times lately. People should stop building UIs, nobody wants to interact with a UI. Everyone's app should just be an API that you can use with a chatbot. Except for my app — my app is a handcrafted miracle of artisanal UX and its UI will change the way you see the world.

It's exactly the old argument, just now with LLMs in the place of shell pipelines: in terms of functionality and value to users, software ought to be malleable and composable. We've known it since the eighties. But the model of selling a piece of software as a product as if it were a pair of shoes is incompatible with that. You need a big monolithic application to justify users paying a bunch of money for it, and you need it to have a fancy interface that makes an impression. And the whole software industry is built on top of that model. Where monolithic software is completely unfit for a purpose, like when it needs to be a component of a larger system, we rely on (mostly unpaid) OSS.

Except now LLMs let you, with very little technical know-how, plaster a ‘programmable’ interface on top of unstructured data/interface meant for humans, and because that's what we actually want, of course everybody does that. So the end result is a wildly expensive pipeline from API to UI and back to API again. I wonder how long the legacy ‘human-oriented’ layer in the middle, and the industry that's been built on top of it, will last.

(Separately, chatbots are not great as a UI for most things, and the problem of building the universal UI still also stands. But it turns out for a lot of things people would rather have a bad universal UI than a good special-purpose UI for each task.)

Older »