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 Effect
s:
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 Aff
1 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 Aff
s 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-parallel
2
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.