One of my favorite features of PureScript is its ability to work with asynchronous effects. While learning the language, I struggled to find any beginner material that introduced the relevant topics and included small examples. This post hopes to fill that gap.

Getting Started with Aff

PureScript’s built-in type for effects is called Effect. However, this type represents synchronous effects. This means if we want expose asynchronous APIs to our users using Effect, we’re stuck with the callback-style. Here’s a contrived example that “slowly” adds two numbers together using Effects:

module Sync where

import Prelude

import Effect (Effect)
import Effect.Class.Console as Console
import Effect.Timer (setTimeout)

slowInt :: Int -> (Int -> Effect Unit) -> Effect Unit
slowInt int cb =
  unit <$ setTimeout 1000 (cb int)

slowAdd :: Int -> Int -> (Int -> Effect Unit) -> Effect Unit
slowAdd a b cb =
  slowInt a \slowA ->
    slowInt b \slowB ->
      cb $ slowA + slowB

main :: Effect Unit
main =
  slowAdd 1 2 \result ->
    Console.logShow result

Yuck. This is especially painful in slowAdd, where we’re composing the results of several callback-based functions. Luckily, there’s a better way – the Aff1 monad. Aff is the type that represents asynchronous effects in PureScript. Let’s get started with a very simple Aff program.

module Basics where

import Prelude

import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class.Console as Console

asyncInt :: Aff Int
asyncInt = pure 1

main :: Effect Unit
main = launchAff_ do
  result <- asyncInt

  Console.logShow result

First things first, we’re now using launchAff_ inside of our main function. Here’s the signature for this function:

launchAff_ :: forall a. Aff a -> Effect Unit

It takes any Aff and returns an Effect with no value. You can think about this as initializing Aff’s scheduler and starting the event loop. Generally, we want all of our PureScript applications to launch an Aff at the very top-level and work completely inside Aff otherwise.

Next, we have the line that calls asyncInt. Note that this line uses the do notation. This works because Aff is a monad. Finally, the implementation of asyncInt simply creates an Aff from the value 1 by calling pure. It’s not really doing anything asynchronous, so technically I’m a liar.

Rewriting Our Effect Program

Now that we have a taste of Aff, let’s rewrite the above slowAdd program using Aff rather than Effect. We’ll only need one helper function – delay (the Aff version of setTimeout).

module Async where

import Prelude

import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (Aff, delay, launchAff_)
import Effect.Class.Console as Console

slowInt :: Int -> Aff Int
slowInt int = do
  delay $ Milliseconds 1000.0

  pure int

slowAdd :: Int -> Int -> Aff Int
slowAdd a b = do
  slowA <- slowInt a
  slowB <- slowInt b

  pure $ slowA + slowB

main :: Effect Unit
main = launchAff_ do
  result <- slowAdd 1 2

  Console.logShow result

I hope we can agree that this looks significantly nicer than the previous Effect-based version. It’s similar to the contrast between JavaScript code written with callbacks and promises. But the value here isn’t just aesthetics.

To demonstrate the power of Aff, let’s convert our example to fetch our two slow ints in parallel. Right now, we can run the program to see that we’re doing things in serial.

$ spago bundle-app -m Async
[info] Build succeeded.
[info] Bundle succeeded and output file to index.js
$ time node index.js
3
node index.js  0.14s user 0.15s system 12% cpu 2.267 total

Note that this program is taking just over 2 seconds to run. We’d like to cut that time to just over one second. To do this, we’ll use the functions forkAff and joinFiber.

module Fork where

import Prelude

import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (Aff, delay, forkAff, joinFiber, launchAff_)
import Effect.Class.Console as Console

slowInt :: Int -> Aff Int
slowInt int = do
  delay $ Milliseconds 1000.0

  pure int

slowAdd :: Int -> Int -> Aff Int
slowAdd a b = do
  fiberA <- forkAff $ slowInt a
  fiberB <- forkAff $ slowInt b

  slowA <- joinFiber fiberA
  slowB <- joinFiber fiberB

  pure $ slowA + slowB

main :: Effect Unit
main = launchAff_ do
  result <- slowAdd 1 2

  Console.logShow result

Let’s look at the signatures for forkAff and joinFiber.

forkAff :: forall a. Aff a -> Aff (Fiber a)

joinFiber :: forall a. Fiber a -> Aff a

The forkAff function takes an Aff and gives us back a Fiber wrapped in an Aff. We can think of a Fiber as a green thread. Forking a Fiber yields control back to our code and the Aff goes on running inside of the Fiber. Once we’re ready to use the result, we can call joinFiber which takes a Fiber and gives us back an Aff. Let’s compile and run the code above to show that we’ve actually parallelized our code.

$ spago bundle-app -m Fork
[info] Build succeeded.
[info] Bundle succeeded and output file to index.js
$ time node index.js
3
node index.js  0.14s user 0.15s system 22% cpu 1.272 total

Cleaner Parallel Code

We can simplify our parallel example even further because Aff has an instance of the Parallel typeclass. This means we can use the parSequence function. I’ll spare you the signature – just think of this function as executing a collection of Affs in parallel and returning an Aff of the results.

module Parallel where

import Prelude

import Control.Parallel (parSequence)
import Data.Foldable (sum)
import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (Aff, delay, launchAff_)
import Effect.Class.Console as Console

slowInt :: Int -> Aff Int
slowInt int = do
  delay $ Milliseconds 1000.0

  pure int

slowAdd :: Int -> Int -> Aff Int
slowAdd a b = do
  results <- parSequence [slowInt a, slowInt b]

  pure $ sum results

main :: Effect Unit
main = launchAff_ do
  result <- slowAdd 1 2

  Console.logShow result

Again, we confirm it runs in parallel:

$ spago bundle-app -m Parallel
[info] Build succeeded.
[info] Bundle succeeded and output file to index.js
$ time node index.js
3
node index.js  0.14s user 0.15s system 23% cpu 1.276 total

You can find more fun parallel functions in the purescript-parallel2 package.

Combining Aff with Other Monads

Last but not least, Aff can be combined with other monads using monad transformers. As stated above, we’re generally running our PureScript programs inside of Aff. This means we want Aff to be at the bottom of our monad transformer stack.

Let’s create an example that slowly adds two numbers together, but throws an error if either of the numbers is greater than 10.

module Stack where

import Prelude

import Control.Monad.Except (ExceptT, runExceptT, throwError)
import Control.Parallel (parSequence)
import Data.Foldable (sum)
import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (Aff, delay, launchAff_)
import Effect.Aff.Class (liftAff)
import Effect.Class.Console as Console

type ErrorAff a = ExceptT String Aff a

slowInt :: Int -> ErrorAff Int
slowInt int = do
  liftAff $ delay $ Milliseconds 1000.0

  if int > 10
    then throwError "too big"
    else pure int

slowAdd :: Int -> Int -> ErrorAff Int
slowAdd a b = do
  results <- parSequence [slowInt a, slowInt b]

  pure $ sum results

main :: Effect Unit
main = launchAff_ do
  result1 <- runExceptT $ slowAdd 1 2

  Console.logShow result1

  result2 <- runExceptT $ slowAdd 10 11

  Console.logShow result2

Describing monad transformers is beyond the scope of this post, but you can see that Aff works quite nicely inside of monad stacks. We’re using the provided function liftAff to “lift” the results of plain old Aff functions into our stack (delay returns an Aff Unit).

Something I find particularly nice about the above code is that parSequence continues to work with no changes even though we’ve updated slowInt to return our type alias ErrorAff rather than Aff.

Here’s the result of running our new program:

$ spago run -m Stack
[info] Build succeeded.
(Right 3)
(Left "too big")

Aff is Awesome

I hope this post has given you a taste of asynchronous programming in PureScript. Aff is a powerful and productive library that makes writing asynchronous code fun again. The power of Aff, which is only a library, is a testament to the flexibility of the PureScript language.