title | author | published |
---|---|---|
Handling Native Effects with the Eff Monad |
Phil Freeman |
2016-05-24 |
In this post, I'm going to talk about PureScript's hybrid approach to handling side-effects.
As in Haskell, values in PureScript do not have side-effects by default, and there are a number of techniques for handling "non-native" side-effects. Such techniques include the use of things like monoids, monads, applicative functors and arrows, but I'm not going to talk about those here. I'm going to talk about how PureScript handles "native" effects, i.e. effects which are provided by the runtime system, and which cannot be emulated by pure functions.
Some examples of native effects are:
- Console IO
- Random number generation
- Exceptions
- Reading/writing mutable state
And in the browser:
- DOM manipulation
- XMLHttpRequest / AJAX calls
- Interacting with a websocket
- Writing/reading to/from local storage
PureScript's purescript-eff
package defines a monad called Eff
, which is used to handle native effects. The goal of the Eff
monad is to provide a typed API for effectful computations, while at the same time generating efficient Javascript.
Let's start with an example:
module RandomExample where
import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Random (random)
import Control.Monad.Eff.Console (logShow)
printRandom = do
n <- random
logShow n
This example requires the purescript-console
and purescript-random
dependencies to be installed:
pulp init
bower install --save purescript-console purescript-random
If you save this file as src/RandomExample.purs
, you will be able to compile and run it using PSCi:
pulp psci
> import RandomExample
> printRandom
...
You should see a randomly selected number between 0 and 1 printed to the console.
This program uses do
-notation to combine two types of native effects provided by the Javascript runtime: random number generation and console IO.
We can inspect the type of printRandom
by using the :type command
> import RandomExample
> :type printRandom
The type of printRandom
will be printed to the console. You should see a type which looks like this:
forall e. Eff (console :: CONSOLE, random :: RANDOM | e) Unit
This type looks quite complicated, but is easily explained by analogy with PureScript's extensible records system.
Consider a simple function which uses extensible records:
fullName person = person.firstName <> " " <> person.lastName
This function creates a full name string from an object containing firstName
and lastName
properties. If you find the type of this function in PSCi as before, you will see this:
forall t. { firstName :: String, lastName :: String | t } -> String
The readable version of this type is "fullName
takes an object with firstName
and lastName
properties and any other properties and returns a String
".
That is, fullName
does not care if you pass an object with more properties, as long as the firstName
and lastName
properties are present:
> fullName { firstName: "Phil", lastName: "Freeman", location: "Los Angeles" }
Phil Freeman
Similarly, the type of printRandom
above can be interpreted as follows: "printRandom
is an effectful computation, which can be run in any environment which supports random number generation and console IO, and any other types of side effect, and which yields a value of type Unit
".
This is the origin of the name "extensible effects": we can always extend the set of side-effects, as long as we can support the set of effects that we need.
This extensibility allows code in the Eff
monad to interleave different types of effects.
The random
function which we used has the following type:
forall e1. Eff (random :: RANDOM | e1) Number
which is not the same as the type of main
.
However, we can instantiate the polymorphic type variable in such a way that the types do match. If we choose e1 ~ (console :: CONSOLE | e)
, then the two rows are equal, up to reordering.
Similarly, logShow
has a type which can be instantiated to match the type of printRandom
:
forall a e2. Show a => a -> Eff (console :: CONSOLE | e2) Unit
This time we have to choose e2 ~ random :: RANDOM | e
.
The key is that we don't have to give a type for printRandom
in order to be able to find these substitutions. psc
will find a most general type for printRandom
given the polymorphic types of random
and logShow
.
Looking at the source code, you will see the following definition for Eff
:
foreign import Eff :: # ! -> * -> *
*
is the usual kind of types, and !
is the kind of effects. The #
kind constructor is used to construct kinds for rows, i.e. unordered, labelled collections.
So Eff
is parameterized by a row of effects, and a return type.
If we were to give a kind to the object type constructor { ... }
, it would have kind # * -> *
. That is, an object type is parameterized by a row of types.
Type annotations are usually not required when using Eff
, since rows of effects can be inferred, but they can be used to indicate to the compiler which effects are expected in a computation.
If we annotate the previous example with a closed row of effects:
main :: Eff (console :: CONSOLE, random :: RANDOM) Unit
main = do
n <- random
logShow n
(note the lack of a type variable here), then we cannot accidentally include a subcomputation which makes use of a different type of effect. This is an advantage of Eff
over Haskell's more coarsely-grained IO
monad.
Rows of effects can also appear on the left-hand side of a function arrow. This is what differentiates actions like logShow
and random
from effect handlers.
While actions add to the set of required effects, a handler subtracts
effects from the set.
Consider catchException
from the purescript-exceptions
package:
catchException
:: forall a e
. (Error -> Eff e a)
-> Eff (err :: EXCEPTION | e) a
-> Eff e a
Note that the type of the effect on the right of the final function arrow requires fewer effects than the effect to its left. Namely, catchException
removes the EXCEPTION
effect from the set of required effects.
This is useful, because the type system can be used to delimit portions of code which require a particular effect, and then to wrap that code in a handler, embedding it inside a piece of code which does not use that effect.
For example, we can write a piece of code which uses exceptions, then wrap that code using catchException
to embed the computation in a piece of code which does not use exceptions.
purescript-eff
also defines the handler runPure
, which takes a computation with no side-effects, and safely evaluates it as a pure value:
type Pure a = Eff () a
runPure :: forall a. Pure a -> a
For example, we can define a version of the division function for which division by zero results in an exception:
module ErrorsExample where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Exception (EXCEPTION, throw)
divide :: forall e. Int -> Int -> Eff (err :: EXCEPTION | e) Int
divide _ 0 = throw "Division by zero"
divide n m = pure (n / m)
If we have already defined this function, we can use the runPure
and catchException
handlers to define a version of divide
which reports its errors using Either
:
import Data.Either
dividePure :: Int -> Int -> Either String Int
dividePure n m = runPure (catchException (pure <<< Left <<< message) (Right <$> divide n m))
Note that after we use catchException
to remove the EXCEPTION
effect, there are no more effects remaining, so we can use runPure
to evaluate the return value.
New effects can be defined using foreign import data
just as in the case of types.
Suppose we wanted to define an effect for incrementing a single shared global counter. We simply declare the kind of our new type constructor to be !
:
foreign import data COUNTER :: !
We can then use our new effect in an action. Primitive actions are usually defined using the FFI, so it is important to understand the underlying model for Eff
-based effects.
A computation of type Eff e a
is implemented in Javascript as a zero-argument function whose body is expected to perform its side effects, before finally returning its result.
We can therefore implement a simple action as follows:
foreign import incrCounter :: forall e. Eff (counter :: COUNTER | e) Number
and in the corresponding native module:
exports.incrCounter = function() {
return ++globalCounter;
};
Note the type we give to incrCounter
: we use a polymorphic type to make sure that Counter
can be interleaved with other effects.
Usually, we wouldn't write a handler for the Counter
effect, since we have no way to guarantee that the globalCounter
hasn't been modified. However, if we wanted to provide an unsafe "escape hatch" for Counter
, we might do so as follows:
foreign import unsafeRunCounter :: forall e a. Eff (counter :: COUNTER | e) a -> Eff e a
And in JavaScript:
exports.unsafeRunCounter = function(f) {
return f;
};
The psc
compiler has special support for the Eff
monad. Ordinarily, a chain of monadic binds might result in poor performance when executed in Node or in the browser. However, the compiler can generate code for the Eff
monad without explicit calls to the monadic bind function >>=
.
Take the random number generation from the start of the post. If we compile this example without optimizations, we end up the following Javascript:
var main =
Prelude[">>="]
(Control_Monad_Eff.monadEff())
(Control_Monad_Eff_Random.random)
(function (n) {
return Control_Monad_Eff_Console.logShow(Prelude.showNumber())(n);
});
However, if we use the default optimizations, the calls to Eff
's monadic bind function are inlined, resulting the following tidier Javascript:
var main = function __do() {
var n = Control_Monad_Eff_Random.random();
return Control_Monad_Eff_Console.logShow(Prelude.showNumber())(n)();
};
While this is a small improvement, the benefit is greater when using multiple nested calls to >>=
.
The improvement is even more marked when optimizations are used in conjunction with tail call elimination. Consider the following recursive program which prints an increasing sequence of numbers to the console:
go n = do
logShow n
go (n + 1)
main = go 1
Without optimizations, the compiler generates the following Javascript, which fails after a few iterations with a stack overflow:
var go =
Prelude[">>="]
(Control_Monad_Eff.monadEff())
(Control_Monad_Eff_Console.logShow(Prelude.showNumber())(n))
(function (_) {
return go(n + 1);
});
However, with optimizations, the Javascript can be made to run without errors:
var go = function (__copy_n) {
return function __do() {
var n = __copy_n;
tco: while (true) {
Control_Monad_Eff_Console.logShow(Prelude.showNumber())(n)();
var __tco_n = n + 1;
n = __tco_n;
continue tco;
};
};
};
The psc
compiler has additional support for one particular native effect, namely the ST
effect, which is used to provide scoped mutable state.
Consider the following function, which computes the total stopping time of the Collatz sequence for a given initial value:
collatz :: Int -> Int
collatz n = pureST do
r <- newSTRef n
count <- newSTRef 0
untilE do
modifySTRef count (_ + 1)
m <- readSTRef r
writeSTRef r $ if m `mod` 2 == 0 then m / 2 else 3 * m + 1
pure (m == 1)
readSTRef count
In this case, psc
notices that the mutable variables r
and count
are scoped by runST
and so can safely be turned into local mutable variables.
The resulting Javascript is surprisingly short:
var collatz = function (n) {
return Control_Monad_Eff.runPure(function __do() {
var r = n;
var count = 0;
(function () {
while (!(function __do() {
count = 1 + count;
var m = r;
r = (m % 2 === 0) ? m / 2 : 3 * m + 1;
return m === 1;
})()) { };
return {};
})();
return count;
});
};
The Eff
monad provides a way to use native effects in PureScript, in such a way that different types of effects can be interleaved, and such that the generated Javascript is relatively simple.