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.