I’ve been learning PureScript for the past few months and really enjoying it. PureScript is a Haskell dervied language that compiles to JavaScript. You can target node for the backend or the browser for frontend code.
One of my favorite features of PureScript is its fantastic interop with JavaScript. This makes the language very pragmatic – though it has a large ecosystem of native packages, anything that’s missing can easily be handled with an existing npm package and interop. PureScript made a big departure from Haskell in order to make this interop more convenient. While Haskell is a lazy language by default, PureScript is strict.
PureScript does have laziness support but you must choose to use it explicitly. In this post, I’ll describe the fundamentals of laziness in PureScript.
Building Blocks
Laziness in PureScript is build on top of the
purescript-lazy
package. It provides two fundamental tools in the Data.Lazy
module – defer
and force
. Let’s talk about defer
first. Here is its signature:
defer :: forall a. (Unit -> a) -> Lazy a
The defer
function is a smart constructor for the Lazy
type. In a nutshell,
if you want to create a lazy value, you give defer
a function that wraps the
value you want to create. You ignore the argument to the function (a Unit
type) and return the value you want to be lazy. Let’s try an example.
import Data.Lazy (Lazy, defer)
inc :: Int -> Lazy Int
inc n = defer \_ -> n + 1
From the repl, we can call inc
to create a lazy incremented integer.
> :t inc 1
Lazy Int
> inc 1
(defer \_ -> 2)
> inc 2
(defer \_ -> 3)
It’s important to note that when the repl prints out the Lazy Int
, it is
actually forcing the computation in order to print the wrapped value. In your
real code, the underlying value won’t be forced until it is required by another
computation or forced manually. You can see from the first line that the return
type of inc
is indeed a Lazy Int
(the :t
statement lets us see the type
of an expression in the repl).
This brings us to the second fundamental function, force
. Here is the
signature:
force :: forall a. Lazy a -> a
We can use force
to force the evaluation of a lazy value. Assuming the inc
definition above, we can do the following in the repl:
> import Prelude
> import Data.Lazy (force)
> :t force $ inc 2
Int
> force $ inc 2
3
The force
function retrieves the lazy value by applying the wrapper function
to the value unit
and returning the result.
Going Further
The Lazy
type offers more than just these building blocks. It is an instance
of many useful type classes, allowing you to easily operate on and compose lazy
values. First off, Lazy
is a Functor
. This means we can apply regular
functions with lazy values.
Suppose we have our existing inc
function that returns a Lazy Int
and
another function that operates on Int
. We can use map
, just like any other
Functor
, to operate on the value “inside” of the Lazy
type.
> import Data.Lazy (defer)
> :t map
forall a b f. Functor f => (a -> b) -> f a -> f b
> :t inc
Int -> Lazy Int
> :t (_ * 10)
Int -> Int
> map (_ * 10) $ inc 1
(defer \_ -> 20)
The <$>
operator is just an infix alias for map
, so we could rewrite this
example as:
> (_ * 10) <$> inc 1
(defer \_ -> 20)
Lazy
is also a Monad
. This means we can chain together functions that accept
regular types and return Lazy
types. To do this, we use bind
or its operator
>>=
(or the flipped variant =<<
).
> :t (>>=)
forall a b m. Bind m => m a -> (a -> m b) -> m b
> inc 2 >>= inc
(defer \_ -> 4)
We can also use the do notation, as we can with any other Monad
.
incAndAdd :: Int -> Int -> Lazy Int
incAndAdd x y = do
x' <- inc x
y' <- inc y
pure $ x' + y'
Let’s call it in the repl.
> incAndAdd 3 4
(defer \_ -> 9)
It’s very important to remember that all of these operations and their results
are lazy. No computation will actually happen until something eventually calls
force
on a Lazy
value.
Lazy
has instances for several other interesting type classes, but the last
I’ll mention is Semigroup
. If the value you’re lazily wrapping is a
Semigroup
, then the lazy value is also a Semigroup
(meaning it supports an
append operation). Let’s try it.
> import Data.Lazy (defer)
> :t (<>)
forall a. Semigroup a => a -> a -> a
> (defer \_ -> [1,2]) <> (defer \_ -> [3,4])
(defer \_ -> [1,2,3,4])
Lazy Lists
PureScript provides a higher-level abstraction on top of these building blocks
with the Data.List.Lazy
module in the purescript-lists
package. First, let’s
look at type relevant type constructors.
> import Data.List.Lazy (List(..), Step(..))
> :t List
> forall t1. Lazy (Step t1) -> List t1
> :t Nil
> forall t1. Step t1
> :t Cons
> forall t1. t1 -> List t1 -> Step t1
A lazy List
(from Data.List.Lazy
) is constructed with a Lazy
Step
. A
Step
is constructed with either a Nil
or a Cons
with an element and
another lazy list. We can use these constructors to build a lazy, infinite list
of the fibonacci sequence.
import Data.Lazy (defer)
import Data.List.Lazy (List(..), Step(..))
fibs :: List Int
fibs = fibs' 0 1
where
fibs' :: Int -> Int -> List Int
fibs' a b =
List $ defer \_ ->
Cons a $ fibs' b (a + b)
To consume a lazy list, we can use standard
Foldable
and
Traversable
functions. If we want to recur over it manually, we can use the provided step
function.
> import Data.List.Lazy (step)
> :t step
forall a. List a -> Step a
The step
function takes a lazy list and “forces” the head element, giving us
back a Step
. Let’s use this to find the nth number from the fibonacci
sequence.
nthFib :: Int -> Maybe Int
nthFib n = nthFib' n $ step fibs
where
nthFib' :: Int -> Step Int -> Maybe Int
nthFib' 0 (Cons h _) = Just h
nthFib' i (Cons _ t) | i > 0 = nthFib' (i - 1) $ step t
nthFib' _ _ = Nothing
More
You can learn more by looking at the documentation for Data.Lazy or Data.List.Lazy.