How does an `Aff` monad work?

Hi,

I’m curious about how does the Aff monad work. I know that JavaScript is (mostly) single-threaded, i.e. only the main thread can do DOM modifications, for instance. Apart from that, we have some async APIs, like Promises and Web Workers. How does Aff compare to them? How asynchronous is Aff really?

2 Likes

I think it’s important to keep a distinction between asynchronous and parallel computations. JavaScript in the browser is constrained to be single threaded, so you’ll never have two different bits of your PureScript code running at the same moment (though I think this rule can be bent on nodejs). But code can be completely asynchronous even in a single-threaded execution model. All it means to be asynchronous is that when your code interfaces with some external resource (network, file I/O, etc.), it releases the CPU to do other things, and just schedules the continuation for when you’ve finished with that resource. In a synchronous environment, the thread will do a “busy wait” while waiting for that external resource, and just tell the CPU to keep spinning in a while loop. Since CPU isn’t the limiting resource for most applications, async often helps your application do many different things at the same time, because the CPU is able orchestrate all of them and keep the bottlenecks on the external resources, while synchronous code can suffer delays as threads are blocked by busy-waits. In a single-threaded environment like javascript, a busy-wait can be a disaster, since nothing else can be done while you wait for that single external resource.

Since JavaScript is single-threaded, it works very, very hard to make sure that all code is asynchronous. It’s actually really challenging to do a synchronous HTTP request in JavaScript at all, for example (whether transpiled from PureScript or otherwise). So Aff is fully asynchronous, same as JavaScript Promises or callbacks. The big difference between Aff and a JavaScript Promise is that a Promise is started as soon as it’s defined, while an Aff isn’t started until you call launchAff or runAff or something similar.

On the side, parallel computing is a completely separate thing, where you make use of multiple cores to run multiple threads at the same time. That might be useful for some heavy data processing or something like that where CPU is actually the bottleneck. Somebody else is going to have to chime in on if/how Aff can be used for parallel computing. I’m pretty sure it’s possible in node, and not possible in the browser. I doubt that it’s the default for node, and would expect you have to do something to enable running code in parallel.

2 Likes

Because I was curious too, at one point in time, I tried to visualize the control flow of Aff.js

6 Likes

Aff is in the same space as Promises, but with additional support for cancellation (async exceptions) and resource management (bracket). It’s cooperative multi-tasking which yields on async constructors (makeAff).

For node specifically, you can try https://github.com/natefaubion/purescript-node-workerbees which is a bit of hack, but really convenient. I used workerbees in purs-tidy to distribute formatting multiple files over several actual threads.

2 Likes

So how do Web Workers (Using Web Workers - Web APIs | MDN) fit into that? The MDN manual claims that

The Worker interface spawns real OS-level threads

and although their capabilities are limited, I’m pretty sure that you can, in fact, do things concurrently, even in the browser (see Worker APIs; for instance, you cannot access the DOM and you have to run the script from URL, so no “arbitrary forking”). I understand there is no library on Pursuit wrapping this API?

Edit: oh, I see, there is purescript-workly - Pursuit, I should check that.

Ah, yep, I stand corrected. I’ve never used Web Workers but that does indeed seem to let you run multiple parallel threads when running in a browser.

To launch a worker thread you actually supply it with a URI of JS script file (or you may pass to it data: encoded source). You can interact with workers using standard messages.

In a real web app scenario, one of the ways to use it can be powered by a bundler: it will bundle the worker-related code, and while runtime it will initialize the worker and provide standard async api to interact with it for example using Promises (which can be then converted to Aff and used within PS main thread). For webpack for example there is a loader. So in this case you’ll have to have some intermediate thin FFI layer between the main thread app and the worker app, but it all should be straightforward.

1 Like

That looks neat, thanks!

What does it mean to yield on async constructor? For instance, how can I reason about programs like:

someFunc :: Aff Unit
someFunc = do
  ref <- liftEffect $ Ref.new 0
  fiber <- forkAff do
    someWork
    liftEffect $ Ref.write 1 ref
  someWork'
  liftEffect $ Ref.write 2 ref
  -- joinFiber fiber
  liftEffect $ Ref.read ref >>= logShow 

What is the output of the program above and how does it depend on someWork and someWork2?

It’s “implementation defined”. Since it’s forked, you should reason about it as if they are truly concurrent, even if there are implementation details that may define a specific ordering on a single threaded runtime. If you need ordering guarantees you should be using something like AVar, otherwise you will be writing very difficult to debug code that is susceptible to race conditions.

In reality, the current Aff interpreter will greedily evaluate all Effect code in a fiber (via liftEffect). That is, an Aff that is only comprised of synchronous Effect code, will run synchronously (liftEffect a *> liftEffect b = liftEffect (a *> b). When it hits an async barrier (anything constructed via makeAff), it will defer to the JS runtime, just like any other callback oriented code in JS.

1 Like

Thanks! So any synchronous Effectful function executed in a context of Aff is “guaranteed” to be executed in one shot to completion, not interleaved with another function?

One synchronous thing at a time is how the js engine works.

3 Likes