Strictness surprises in PureScript Lazy lists

As of March 2020, School of Haskell has been switched to read-only mode.

PureScript a Haskell-like language for a JavaScript substrate

PureScript is a Haskell like programming language with some features biaised towards its main compilation backend which is JavaScript.

Between them strictness by default, JavaScript primitive types and literals where rectangular brackets are for the type Array instead of List, and some other differences.

$ pulp repl

PSCi, version 0.11.7
Type :? for help
import Prelude

> :t [1,2,3]
Array Int

> :t true 
Boolean

> :t 1.5
Number 

> :t unit
Unit

-- ranges don't use brackets and are defined for Arrays and for Lists

> import Data.Array ((..)) 
> :t 1..5                  
Array Int

Building Lazy Lists in PureScript from recursive definitions

There is a Data.Enum module that defines the finite sequences enumFromTo and enumFromThenTo but not the infinite ones enumFrom and enumFromThen, and looking for a solution I have encountered some problems.

The lists are defined in a non Prelude package called purescript-lists

There are strict list modules Data.List, Data.List.NonEmpty and lazy lists ones Data.List.Lazy and Data.List.Lazy.NonEmpty based on a Lazy type wrapper defined in purescript-lazy Data.Lazy module.

Trying to define a Lazy sequence with a stop point seems to work apparently without problems but, defining one on Int(s) without stop point, makes the stack overflow.

> import Data.List.Lazy

> 
> :paste
… f :: Int -> List Int
… f x | x > 0 = x : f (x-1)   
…     | otherwise = nil
…

> show $ take 3 $ f 9
"fromStrict ((Cons 9 (Cons 8 (Cons 7 Nil))))"

> :paste
… g :: Int -> List Int
… g x = x : g (x+1)   
…

> show $ take 3 $ g 0

/home/gabi64/lleng/purescript/soh/lazy2/.psci_modules/node_modules/$PSCI/index.js:13
var g = function (x) {
                 ^
RangeError: Maximum call stack size exceeded

>

It seems that strictness is playing a dirty trick here, evaluating the second cons parameter beforehand against our interest, but strict evaluation is in the nature of PureScript.

But if we use the cons function definition expression with the Data.Lazy defer function, instead of going with the (:) operator, the evaluation of the recursive case will be effectively deferred.


-- | cons as defined In Data.List.Lazy

cons :: forall a. a -> List a -> List a
cons x xs = List $ defer \_ -> Cons x xs  -- Cons is a constructor of the type Step

-- defining our function the same way

> import Data.Lazy

> h x = List $ defer \_ -> Cons x $ h (x+1)

> show $ take 3 $ h 0
"fromStrict ((Cons 0 (Cons 1 (Cons 2 Nil))))"

Now it works!

Let's try defining enumFrom

> import Data.Maybe
> import Data.Enum

> :paste
… enumFrom :: forall a. Enum a => a -> List a
… enumFrom x = List $ defer \_ -> case succ x of
…                                   Just x' -> Cons x $ enumFrom x'
…                                   Nothing -> Cons x nil
… 
> show $ take 3 $ enumFrom 0
"fromStrict ((Cons 0 (Cons 1 (Cons 2 Nil))))"

> show $ take 3 $ enumFrom 'A'
"fromStrict ((Cons 'A' (Cons 'B' (Cons 'C' Nil))))"

Let's try defining enumFromThen that corresponds in haskell to a stepped Enum sequence specifying the second value:

(it requires the class BoundedEnum that defines toEnum and fromEnum but there is no instance for Ints)

> import Data.Lazy
> import Data.Maybe
> import Data.Enum
> import Data.List.Lazy

> :paste
… enumFromThen :: forall a. BoundedEnum a => a -> a -> List a
… enumFromThen x y = List $ defer \_ -> 
…                let step = fromEnum y - fromEnum x
…                    mbZ = toEnum (fromEnum y + step) :: Maybe a 
…                in case mbZ of
…                         Just z -> Cons x $ enumFromThen y z
…                         Nothing -> Cons x nil
…                                  
> show $ take 3 $ enumFromThen 'E' 'C'
"fromStrict ((Cons 'E' (Cons 'C' (Cons 'A' Nil))))"

To add an instance for Ints, because orphan instances are disallowed the only way is to define it in the EnumBounded class module making a custom version.

-- added to a local copy of Data.Enum

instance boundedEnumInt :: BoundedEnum Int where
  cardinality = Cardinality (top - bottom + 1)  -- overflowed but unused unless you use Ints as components of compound BoundedEnum instances
  
  fromEnum = id
  toEnum = Just

Now the package manager bower complaints that my local "Data.Enum" module name collides with the installed purescript-enums one.

bower uninstall purescript-enums

The psc-package packaging system is a better option as it is based, à la stackage, on sets of libraries that are guaranteed compatible.

Adding a psc-package project file.

git clone https://github.com/purescript/psc-package
cd psc-package
stack install

cd path/to/my-project
psc-package init  # interactively build the project file

# to start a new psc-package project: pulp --psc-package init 

The project file psc-package.json after adding some dependencies (with psc-package install pkgName):

{
  "name": "lazy4",
  "set": "psc-0.11.7",
  "source": "https://github.com/purescript/package-sets.git",
  "depends": [
    "monoid",
    "foldable-traversable",
    "console",
    "prelude",
    "lists",
    "strings",
    "lazy"
  ]
}

Testing for Int sequences:

module Main where

import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)

import Data.List.Lazy (List(..), nil, take, Step(Cons))
import Data.Lazy (defer)
import Data.Enum (class BoundedEnum, class Enum, fromEnum, succ, toEnum)
import Data.Maybe (Maybe(..))

enumFrom :: forall a. Enum a => a -> List a
enumFrom x = List $ defer \_ -> case succ x of
                                  Just x' -> Cons x $ enumFrom x'
                                  Nothing -> Cons x nil

enumFromThen :: forall a. BoundedEnum a => a -> a -> List a
enumFromThen x y = List $ defer \_ -> 
               let step = fromEnum y - fromEnum x
                   mbZ = toEnum (fromEnum y + step) :: Maybe a  
               in case mbZ of
                        Just z -> Cons x $ enumFromThen y z
                        Nothing -> Cons x nil
                                 
main :: forall e. Eff (console :: CONSOLE | e) Unit

main = log $ show $ take 5 $ enumFromThen 1 3

Output:

$ pulp --psc-package run

Compiling Data.Enum
Compiling Main
* Build successful.
fromStrict ((Cons 1 (Cons 3 (Cons 5 (Cons 7 (Cons 9 Nil))))))

Avoiding unneeded heavy computations with Lazy parameters

$ pulp repl
PSCi, version 0.11.7
Type :? for help

import Prelude

> import Data.Maybe (Maybe(..))
> import Data.Lazy (Lazy, defer, force)
> import Math (sqrt)

> :paste
… heavyComp :: forall a. Boolean -> a {- default -} -> Lazy a -> a

… heavyComp false def _ = def
… heavyComp true _ x = force x
… ^D

> heavyComp false 1.41 $ defer \_ -> sqrt 2.0
1.41

> heavyComp true 1.41 $ defer \_ -> sqrt 2.0
1.4142135623730951