This post is targeted to people who are getting started or are interested in Purescript. There are some parallels with things that exist in Javascript, but not really, since there are no real facilities to make such generic interfaces possible in Javascript.
Let's get started.
Callbacks with Eff
This is probably the most common thing you'll need done -- the resolution of a single value. The simplest way to do this is to use a callback, so you can have a value computed and have your program continue when that value is ready.
It's more than likely that you'll be using some existing Javascript libraries that take callbacks for async. For each {name}.purs
file you have, a corresponding {name}.js
file can be used for foreign functions that you want to use. Like so:
// Main.js
exports.foreignCalculateLength = function(array) {
return function (callback) {
return callback(array.length);
};
};
foreign import foreignCalculateLength :: forall e a.
Array a ->
(Int -> Eff e Unit) ->
Eff e Unit
So you can see that our foreignCalculateLength
function takes an Array a
to return a function that will then take a function that takes Int
to return an Eff
ect of Unit
(basically nothing). Registering a callback returns Eff e Unit
, which means that we need to run this somewhere that can "unwrap" Eff
to pull out our (gooey burrito guts | insert your metaphor here)
.
This can then be used in a main function:
type Main e = Eff (console :: CONSOLE | e) Unit
mainCallback :: forall e. Main e
mainCallback =
foreignCalculateLength [1,2,3] (\x -> do
logShow (x * 2) -- prints "6"
)
We've calculated the length of our array and passed in a callback to display that length times two!
We can write this Eff callback in pure Purescript also.
calculateLengthEff :: forall e a.
Array a ->
(Int -> Eff e Unit) ->
Eff e Unit
calculateLengthEff l s = do
s (length l)
pure unit
Note: You'll see that we are ending our do
block with pure unit
, which matches the return type of Eff e Unit
. This is because Eff
has an instance of the Applicative
typeclass, which means that the member method pure
of Applicative
can be used. pure
is of type a -> f a
, which means that we can "wrap" any value (in this case, unit :: Unit
to Eff
).
And just like in JS, you can run another callback inside of a callback to combine the results later:
mainCallback2 :: forall e. Main e
mainCallback2 =
foreignCalculateLength [1,2,3] (\x -> do
calculateLengthEff [4,5,6] (\y -> do
logShow (x + y)
)
)
We'll learn a better way to do this looking at Aff
s.
Asynchronous Effects, or "Affects"
Aff
s are relatively easy to construct, and may remind you of something you've used in JS, but there are some key differences to note. Let's look at how they work and then talk about it. First, some ways they're made:
- Converting an existing callback Eff to an Aff:
calculateLengthAff :: forall e a. Array a -> Aff e Int
calculateLengthAff l = makeAff \error success -> foreignCalculateLength l success
- Calling the success handler directly:
calculateLengthAff' :: forall e a. Array a -> Aff e Int
calculateLengthAff' l = makeAff \error success -> do
success (length l)
- Using the
Applicative
instance to easily wrap a value in an Aff:
calculateLengthAff'' :: forall e a. Array a -> Aff e Int
calculateLengthAff'' l = pure (length l)
Sort of like Eff
s, you can use do
blocks with Aff
s, but you have to use launchAff
to use them. Like so:
type MainAff e = Eff (err :: EXCEPTION, console :: CONSOLE | e) (Canceler (console :: CONSOLE | e))
mainAff :: forall e. MainAff e
mainAff = launchAff do
x <- calculateLengthAff [1,2,3]
liftEff' (logShow (x * 2))
You'll see that launchAff
returns a Canceler
, which can be used to cancel Aff
s if needed. This can become useful in cancelling HTTP requests and other things, but isn't too important for our uses right now.
Remember that example before where we needed to run callbacks in succession? With Aff
s, that becomes really easy:
mainAff2 :: forall e. MainAff e
mainAff2 = launchAff do
x <- calculateLengthAff [1,2,3]
y <- calculateLengthAff' [4,5]
z <- calculateLengthAff' [6]
liftEff' (logShow (x + y + z))
And the one where we want to multiply the result by 2? Sure, we could multiply the value where we log the value, but what if we wanted to make it part of the pipeline that could be reused? No problem, as we can use the Functor instance to map/<$>
of the type (a -> b) -> f a -> f b
.
mainAff3 :: forall e. MainAff e
mainAff3 = launchAff do
result <- (*) 2 <$> (calculateLengthAff [1,2,3])
liftEff' (logShow result) -- prints "6"
What about if we wanted to take the three successive Aff
s and... run them in parallel? That we can, since Aff
has an instance of MonadPar from purescript-parallel, which in turn has an instance of Apply, which lets us put these parallel processes together using apply/<*>
.
mainAff4 :: forall e. MainAff e
mainAff4 = launchAff do
result <- runParallel $ (\a b c -> a + b + c)
<$> parallel (calculateLengthAff [1,2,3])
<*> parallel (calculateLengthAff' [4,5])
<*> parallel (calculateLengthAff'' [6])
liftEff' (logShow result)
So we can see that doing things that are commonly thought to be hard is actually fairly easy with Purescript thanks to the common interfaces available.
In the majority of your Purescript applications and the libraries you use, Aff
is the answer for single-value async that allows you to write very simple code using do
blocks.
Aside: you have probably noticed that Aff
s remind you of Promises in Javascript. Unfortunately, Promises are eager and cannot be reused in the same way*. They also expose their own specific methods like most Javascript objects, so there is no way to generically transform Promises in a similar fashion as with Arrays or other structures. Lastly, they do not currently provide any method for cancellation, though there are proposals to dramatically change Javascript and Promises themselves to support cancellation that may land in the following years on newer environments.
* for example, you could not do this with Promises:
mainAff5 :: forall e. MainAff e
mainAff5 = launchAff do
let aff = calculateLengthAff [1,2,3]
result <- runParallel $ (\x y z -> x + y + z)
<$> parallel aff
<*> parallel ((_ - 1) <$> aff)
<*> parallel ((_ - 2) <$> aff)
liftEff' (logShow result) -- prints "6"
Multiple async values with Observables
This is an area in which there are multiple solutions, but my favorite libraries for 0-N async values are reactive programming libraries, especially observable libraries.
Like Aff
s and other structures, Observable libraries have three stages that the user cares about: 1) Creation (creating an Observable), 2) Subscription (subscribing to an Observable), 3) Disposal (disposing of a subscription). Of course, the first two cases are the most important.
Purescript-observable is one such library for this in Purescript.
Now, there isn't a super straight-forward way to convert an Aff
into an Observable
, so I made a small library for this. Using this library, we can create Observable
s in a similar fashion as with Aff
s:
calculateLengthObs :: forall e a. Array a -> Eff (observable :: OBSERVABLE | e) (Observable Int)
calculateLengthObs l = liftAff (calculateLengthAff l)
calculateLengthObs' :: forall e a. Array a -> Eff (observable :: OBSERVABLE | e) (Observable Int)
calculateLengthObs' l = observable \sink -> do
sink.next (length l)
sink.complete
free [] -- for returning inner subscriptions that must be disposed when this observable is disposed.
calculateLengthObs'' :: forall a. Array a -> Observable Int
calculateLengthObs'' l = pure (length l)
And then we just apply what is basically the same code as with Aff
s:
type MainObs e = Eff (observable :: OBSERVABLE, console :: CONSOLE | e) (Subscription (console :: CONSOLE | e))
mainObs :: forall e. MainObs e
mainObs = do
s <- calculateLengthObs [1,2,3]
subscribe
{ next : \x -> logShow (x * 2)
, error: message >>> logShow
, complete: pure unit
}
s
mainObs2 :: forall e. MainObs e
mainObs2 = do
s <- calculateLengthObs [1,2,3]
subscribe
{ next : logShow
, error: message >>> logShow
, complete: pure unit
}
((*) 2 <$> s)
mainObs3 :: forall e. MainObs e
mainObs3 = do
s1 <- calculateLengthObs [1,2,3]
s2 <- calculateLengthObs' [4,5]
let s3 = calculateLengthObs'' [6]
subscribe
{ next: logShow
, error: message >>> logShow
, complete: pure unit
}
((\x y z -> x + y + z) <$> s1 <*> s2 <*> s3)
Note that with Observables, you have three callbacks you must provide. next
is for handling each emitted value; error
is for handling each emitted error; complete
is what must be run after the Observable has signaled that it has completed and has no more values to emit. But otherwise, the ideas are largely the same.
I even wrote a wrapper library around another Observable stream library called purescript-xstream
(the original xstream
written by a friend who has a love for incredibly cheesy names) which has a similar API with some more things for convenience:
calculateLengthXStream :: forall e a. Array a -> Eff (stream :: STREAM, ref :: REF | e) (Stream Int)
calculateLengthXStream l = fromAff (calculateLengthAff l)
calculateLengthXStream' :: forall a e. Array a -> Eff (stream :: STREAM | e) (Stream Int)
calculateLengthXStream' l =
create
{ start: \o -> do
o.next (length l)
o.complete unit
, stop: const (pure unit)
}
mainXStream :: forall e.
Eff
( stream :: STREAM
, ref :: REF
, console :: CONSOLE
| e
)
Unit
mainXStream = do
s1 <- calculateLengthXStream [1,2,3]
s2 <- calculateLengthXStream' [4,5]
addListener
{ next: logShow
, error: message >>> logShow
, complete: const (pure unit)
}
((+) <$> s1 <*> ((+) 1 <$> s2))
Conclusion
So I hope this has covered the most common async cases that you will run into when using Purescript and that you've been convinced this is pretty fun and fairly easy (and maybe even cool, for some definitions of cool).
If you read this far, thanks for reading! Please let me know if you have any corrections/critiques/comments/reactions/responses/hatemail/concerns/praises/suggestions on twitter.
Thanks!
Links
- Code for this post: https://github.com/justinwoo/purescript-async-is-easy
- Purescript by Example: https://leanpub.com/purescript/read
- purescript-aff: https://github.com/slamdata/purescript-aff
- purescript-observable: https://github.com/bodil/purescript-observable
- purescript-xstream: https://github.com/justinwoo/purescript-xstream