Laziness in PureScript

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

> force $ inc 2

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
    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
    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


You can learn more by looking at the documentation for Data.Lazy or Data.List.Lazy.