This README file is a crash course on PureScript targeted at Elm developers. It is based on information picked from:
- https://github.com/alpacaaa/elm-to-purescript-cheatsheet
- https://github.com/purescript/documentation
- https://book.purescript.org
- https://jordanmartinez.github.io/purescript-jordans-reference-site/
- https://learnxinyminutes.com/docs/purescript/
- https://gist.github.com/justinwoo/0118be3e4a0d7394a99debbde2515f9b
- https://www.tobyhobson.com/posts/cats/
- https://github.com/alpacaaa/zero-bs-haskell
Sometimes I did a shameless copy-paste instead of writing a bad paraphrase. I think it is fair use but please let me know if I am infringing any copyright. Feel free to open an issue if you find a mistake.
Happy monad lifting! 🏋
Laurent
- PureScript Language Support: Syntax highlighting for the PureScript programming language
- PureScript IDE: PureScript IntelliSense, tooltip, errors, code actions with language-server-purescript/purs IDE server
- Purty: PureScript formatter
Spago is the PureScript package manager and build tool.
The PureScript compiler (transpiler) is quite fast. The compiler messages are not as friendly as with Elm, unfortunately.
Pursuit is the home of PureScript packages documentation. It lets you search by package, module, and function names, as well as approximate type signatures.
Elm | Purescript | Notes |
---|---|---|
String | Data.String | |
Maybe | Data.Maybe | |
Result | Data.Either | Err is Left and Ok is Right |
Array | Data.Array | [] is the empty array |
List | Data.List | Nil is the empty list |
Tuple | Data.Tuple | |
Dict | Data.Map | |
Set | Data.Set | |
() | Data.Unit | () is the empty Row type in PureScript |
Never | Data.Void | |
Debug | Debug.Trace | Debug.spy is the closest thing to Debug.log |
Elm | Purescript | Notes |
---|---|---|
() |
unit | |
identity | identity | |
always | const | |
never | absurd | |
toString | show | |
>> |
>>> |
|
<< |
<<< |
|
|> |
# |
|
<| |
$ |
|
++ |
<> |
Semigroup concatenation (String , Array , List , Tuple …) |
Type signatures are separated with double colons.
sum :: Int -> Int -> Int
Polymorphic functions in PureScript require an explicit forall
to declare type variables before using them.
map :: forall a b. (a -> b) -> Maybe a -> Maybe b
runApp :: Foo -> Bar Baz String Int Unit -> ?x
If you’re not sure of a type in a type signature, you can write a type "hole" consisting of a question mark followed by a lowercase name. The compiler will generate an error and tell you what type it inferred. Note that to use type holes there must be no other compiler errors.
You can use type holes everywhere:
foo :: Int
foo = 1 + ?what_could_this_be
In PureScript, arrays are the most common data structure for sequences of items. They are constructed with square brackets.
import Data.Array ()
myArray = [2,4,3]
-- Cons (prepend)
myNewArray = 1 : [2,4,3] -- [1,2,4,3]
head [1,2,3,4] -- (Just 1)
tail [1,2,3,4] -- (Just [2,3,4])
init [1,2,3,4] -- (Just [1,2,3])
last [1,2,3,4] -- (Just 4)
-- Array access by index starting at 0
[3,4,5,6,7] !! 2 -- (Just 5)
-- Range
1..5 -- [1,2,3,4,5]
length [2,2,2] -- 3
drop 3 [1,2,3,4,5] -- [4,5]
take 3 [1,2,3,4,5] -- [1,2,3]
append [1,2,3] [4,5,6] -- [1,2,3,4,5,6]
You can use pattern matching for arrays of a fixed length:
isEmpty :: forall a. Array a -> Boolean
isEmpty [] = true
isEmpty _ = false
takeFive :: Array Int -> Int
takeFive [0, 1, a, b, _] = a * b
takeFive _ = 0
For performance reasons, PureScript does not provide a direct way of destructuring arrays of an unspecified length. If you need a data structure which supports this sort of matching, the recommended approach is to use lists.
Another way is to use uncons
or unsnoc
to break an array into its first or last element and remaining elements:
import Data.Array (uncons, unsnoc)
uncons [1, 2, 3] -- Just {head: 1, tail: [2, 3]}
uncons [] -- Nothing
unsnoc [1, 2, 3] -- Just {init: [1, 2], last: 3}
unsnoc [] -- Nothing
case uncons myArray of
Just { head: x, tail: xs } -> somethingWithXandXs
Nothing -> somethingElse
Beware unsnoc
is O(n) where n is the length of the array.
Be careful! The literal [1,2,3]
has a type of List Int
in Elm but Array Int
in Purescript.
PureScript lists are linked lists. You can create them using the Cons
infix alias :
and Nil
when there is no link to the next element (end of the list).
myList = 1 : 2 : 3 : Nil
myNewList = 1 : myList
Another way to create a list is from a Foldable structure (an Array in this case):
myList = List.fromFoldable [2,4,3]
case xs of
Nil -> ... -- empty list
x : rest -> ... -- head and tail
Data.Foldable contains common functions (sum
, product
, minimum
, maximum
etc.) for data structures which can be folded, such as Array
and List
.
There is a Data.NotEmpty module that defines a generic NonEmpty
data structure. :|
is the infix alias for its constructor.
This quite useful to flatten cases as described in the famous "Parse, don’t validate" blog post:
import Data.NonEmpty (NonEmpty, (:|))
-- no Maybe when getting the head
arrayHead :: NonEmpty Array a -> a
arrayHead (x :| _) = x
Instead the generic Data.NonEmpty
module, use specific modules when possible:
- Data.Array.NonEmpty to create
NonEmptyArray
- Data.List.NonEmpty to create
NonEmptyList
For convenience, Data.Array.NonEmpty.Internal provides the internal constructor NonEmptyArray
. Beware you can create a NonEmptyArray
that is actually empty with it so use this at your own risk when you know what you are doing.
Tuples are just a data type in Purescript. Use records when possible.
import Data.Tuple (Tuple(..), fst, snd)
coords2D :: Tuple Int Int
coords2D = Tuple 10 20
getX :: Tuple Int Int -> Int
getX coords = fst coords
getY :: Tuple Int Int -> Int
getY coords = snd coords
You can use tuples that are not restricted to two elements with Data.Tuple.Nested. All nested tuple functions are numbered from 1 to 10:
import Data.Tuple.Nested (Tuple3, tuple3, get2)
coords3D :: Tuple3 Int Int Int
coords3D = tuple3 10 20 30
getY :: Tuple3 Int Int Int -> Int
getY coords = get2 coords
/\
is the infix alias for Tuple
that allows nested tuples of arbitrary length (depth). The same alias exists for types. The previous example could be rewritten as:
import Data.Tuple.Nested (type (/\), (/\), get2)
coords3D :: Int /\ Int /\ Int
coords3D = 10 /\ 20 /\ 30
getY :: Int /\ Int /\ Int -> Int
getY coords = get2 coords
distance2D :: Tuple Int Int -> Int
distance2D (Tuple x y) =
x * x + y * y
distance3D :: Int /\ Int /\ Int -> Int
distance3D (x /\ y /\ z) =
x * x + y * y + z * z
type Person =
{ name :: String
, age :: Int
}
myPerson :: Person
myPerson = { name: "Bob", age: 30 }
edited :: Person
edited = myPerson { age = 31 }
toPerson :: String -> Int -> Person
toPerson name age =
{ name: name, age: age }
toPerson2 :: String -> Int -> Person
toPerson2 name age =
{ name, age }
toPerson3 :: String -> Int -> Person
toPerson3 =
{ name: _, age: _ } -- equivalent to `\name age -> { name, age }` (types inferred by the signature)
In PureScript (_ + 5)
is the same as (\n -> n + 5)
, so (_.prop)
is the same as (\r -> r.prop)
.
_.age myPerson -- 30
_.address.street myPerson -- "Main Street"
personName :: Person -> String
personName { name } = name
bumpAge :: Person -> Person
bumpAge p@{ age } =
p { age = age + 1 }
ecoTitle {author: "Umberto Eco", title: t} = Just t
ecoTitle _ = Nothing
ecoTitle {title: "Foucault's pendulum", author: "Umberto Eco"} -- (Just "Foucault's pendulum")
ecoTitle {title: "The Quantum Thief", author: "Hannu Rajaniemi"} -- Nothing
-- ecoTitle requires both field to type check
ecoTitle {title: "The Quantum Thief"} -- Object lacks required property "author"
Row Polymorphism is the equivalent of the extensible records concept in Elm.
-- Elm
getAge : { a | age : Int } -> Int
getAge { age } = age
-- PureScript
getAge :: forall r. { age :: Int | r } -> Int
getAge { age } = age
In the above example, the type variable r
has kind Row Type
(an unordered collection of named types, with duplicates).
The where
clause is "syntactic sugar" for let bindings. Functions defined below the where
keyword can be used in the function scope and in the where
scope.
foo :: String -> String -> String
foo arg1 arg2 =
bar arg1 arg2 "Welcome to PureScript!"
where
bar :: String -> String -> String -> String
bar s1 s2 s3 =
(baz s1) <> (baz s2) <> s3
baz :: String -> String
baz s = "Hi " <> s <> "! "
Guards consist of lines starting with |
followed by a predicate. They can be used to make function definitions more readable:
greater x y
| x > y = true
| otherwise = false
Exhaustibility of patterns is checked by the compiler. To be considered exhaustive, guards must clearly include a case that is always true. otherwise
is a synonym for true
and is commonly used in guards.
Guards may also be used within case expressions, which allow for inline expressions. For example, these are equivalent:
fb :: Int -> Effect Unit
fb = log <<< case _ of
n
| 0 == mod n 15 -> "FizzBuzz"
| 0 == mod n 3 -> "Fizz"
| 0 == mod n 5 -> "Buzz"
| otherwise -> show n
fb :: Int -> Effect Unit
fb n = log x
where
x
| 0 == mod n 15 = "FizzBuzz"
| 0 == mod n 3 = "Fizz"
| 0 == mod n 5 = "Buzz"
| otherwise = show n
Instead of Elm’s type
, PureScript uses data
.
Instead of Elm’s type alias
, PureScript uses type
.
-- Elm
type Direction = Up | Down
type alias Time = Int
-- PureScript
data Direction = Up | Down
type Time = Int
Instead of
fullName :: String -> String -> String
fullName firstName lastName =
firstName <> " " <> lastName
fullName "Phillip" "Freeman" -- "Phillip Freeman"
fullName "Freeman" "Phillip" -- "Freeman Phillip" wrong order!
we could write more explicit types but that would not prevent arguments ordering errors:
type FirstName = String
type LastName = String
type FullName = String
fullName :: FirstName -> LastName -> FullName
fullName firstName lastName =
firstName <> " " <> lastName
fullName "Phillip" "Freeman" -- "Phillip Freeman"
fullName "Freeman" "Phillip" -- "Freeman Phillip" still wrong order!
Instead can use single constructor data types and destructure them to ensure the right arguments are provided:
data FirstName = FirstName String
data LastName = LastName String
data FullName = FullName String
fullName :: FirstName -> LastName -> FullName
fullName (FirstName firstName) (LastName lastName) =
firstName <> " " <> lastName
fullName (FirstName "Phillip") (LastName "Freeman") -- "Phillip Freeman"
fullName (LastName "Freeman") (FirstName "Phillip") -- compiler error!
For the compiler to optimize the output for this common pattern, it is even better to use the newtype
keyword which is especially restricted to a single constructor which contains a single argument.
newtype FirstName = FirstName String
newtype LastName = LastName String
newtype FullName = FullName String
Newtypes are especially useful when dealing with raw data as you can write a "validation" function without exposing the type constructor itself in exports. This is known as the smart constructor pattern:
module Password
( Password -- not Password(..) to prevent exposing the Password constructor
, toPassword
) where
newtype Password = Password String
toPassword :: String -> Either String Password
toPassword str =
if length str >= 6 then
Right (Password str)
else
Left "Size should be at least 6"
myPassword = toPassword "123456"
Here is a full example shamelessly ripped off from the unmissable PureScript: Jordan's Reference:
module Syntax.Module.FullExample
-- exports go here by just writing the name
( value
, function, (>@>>>) -- aliases must be wrapped in parenthesis
-- when exporting type classes, there are two rules:
-- - you must precede the type class name with the keyword 'class'
-- - you must also export the type class' function (or face compilation errors)
, class TypeClass, tcFunction
-- when exporting modules, you must precede the module name with
-- the keyword 'module'
, module ExportedModule
-- The type is exported, but no one can create a value of it
-- outside of this module
, ExportDataType1_ButNotItsConstructors
-- syntax sugar for 'all constructors'
-- Either all or none of a type's constructors must be exported
, ExportDataType2_AndAllOfItsConstructors(..)
-- Type aliases can also be exported
, ExportedTypeAlias
-- When type aliases are aliased using infix notation, one must export
-- both the type alias, and the infix notation where 'type' must precede
-- the infix notation
, ExportedTypeAlias_InfixNotation, type (<|<>|>)
-- Data constructor alias; exporting the alias requires you
-- to also export the constructor it aliases
, ExportedDataType3_InfixNotation(Infix_Constructor), (<||||>)
, ExportedKind
, ExportedKindValue
) where
-- imports go here
-- imports just the module
import Module
-- import a submodule
import Module.SubModule.SubSubModule
-- import values from a module
import ModuleValues (value1, value2)
-- imports functions from a module
import ModuleFunctions (function1, function2)
-- imports function alias from a module
import ModuleFunctionAliases ((/=), (===), (>>**>>))
-- imports type class from the module
import ModuleTypeClass (class TypeClass)
-- import a type but none of its constructors
import ModuleDataType (DataType)
-- import a type and one of its constructors
import ModuleDataType (DataType(Constructor1))
-- import a type and some of its constructors
import ModuleDataType (DataType(Constructor1, Constructor2))
-- import a type and all of its constructors
import ModuleDataType (DataType(..))
-- resolve name conflicts using "hiding" keyword
import ModuleNameClash1 (sameFunctionName1)
import ModuleNameClash2 hiding (sameFunctionName1)
-- resolve name conflicts using module aliases
import ModuleNameClash1 as M1
import ModuleNameClash2 as M2
-- Re-export modules
import Module1 (anInt1) as Exports
import Module2 (anInt2) as Exports
import Module3 (anInt3) as Exports
import Module4.SubModule1 (someFunction) as Exports
import ModuleKind (ImportedKind, ImportedKindValue) as Exports
import Prelude
import ExportedModule
-- To prevent warnings from being emitted during compilation
-- the above imports have to either be used here or
-- re-exported (explained later in this folder).
value :: Int
value = 3
function :: String -> String
function x = x
infix 4 function as >@>>>
class TypeClass a where
tcFunction :: a -> a -> a
-- now 'sameFunctionName1' refers to ModuleF1's function, not ModuleF2's function
myFunction1 :: Int -> Int
myFunction1 a = sameFunctionName1 a
myFunction2 :: Int -> Int
myFunction2 a = M1.sameFunctionName1 (M2.sameFunctionName1 a)
dataDifferences :: M1.SameDataName -> M2.SameDataName -> String
dataDifferences M1.Constructor M2.Constructor = "code works despite name clash"
data ExportDataType1_ButNotItsConstructors = Constructor1A
data ExportDataType2_AndAllOfItsConstructors
= Constructor2A
| Constructor2B
| Constructor2C
type ExportedTypeAlias = Int
data ExportedDataType3_InfixNotation = Infix_Constructor Int Int
infixr 4 Infix_Constructor as <||||>
type ExportedTypeAlias_InfixNotation = String
infixr 4 type ExportedTypeAlias_InfixNotation as <|<>|>
data ExportedKind
foreign import data ExportedKindValue :: ExportedKind
The show
function takes a value and displays it as a string. show
is defined by a type class in the Prelude module called Show, which is defined as follows:
class Show a where
show :: a -> String
This code declares a new type class called Show
, which is parameterized by the type variable a
.
A type class instance contains implementations of the functions defined in a type class, specialized to a particular type. You can add any type to a class, as long as you define the required functions.
For example, here is the definition of the Show
type class instance for Boolean
values, taken from the Prelude. We say that the Boolean
type belongs to the Show
type class.
instance Show Boolean where
show true = "true"
show false = "false"
Instead of defining a different map
for each type (Maybe, Result etc.) like in Elm, PureScript uses type classes.
For instance, map
is defined once for all with the Functor
type class. A Functor
is a type constructor which supports a mapping operation map
.
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
The compiler can derive type class instances to spare you the tedium of writing boilerplate. There are a few ways to do this depending on the specific type and class being derived.
Since PureScript version 0.15.0, giving class instances a name (for generated code readability) is optional. It it generated by the compiler if missing.
Some classes have special built-in support (such as Eq
), and their instances can be derived from all types.
For example, if you you'd like to be able to remove duplicates from an array of an ADT using nub
, you need an Eq
and Ord
instance. Rather than writing these manually, let the compiler do the work.
import Data.Array (nub)
data MyADT
= Some
| Arbitrary Int
| Contents Number String
derive instance Eq MyADT
derive instance Ord MyADT
nub [Some, Arbitrary 1, Some, Some] == [Some, Arbitrary 1]
Currently (in PureScript version 0.15.12), instances for the following classes can be derived by the compiler:
- Data.Generic.Rep (class Generic) see below
- Data.Newtype (class Newtype)
- Data.Eq (class Eq)
- Data.Ord (class Ord)
- Data.Functor (class Functor)
- Data.Functor.Contravariant (class Contravariant)
- Data.Bifunctor (class Bifunctor)
- Data.Bifoldable (class Bifoldable)
- Data.Bitraversable (class Bitraversable)
- Data.Profunctor (class Profunctor)
If you would like your newtype to defer to the instance that the underlying type uses for a given class, then you can use newtype deriving via the derive newtype
keywords.
For example, let's say you want to add two Score
values using the Semiring
instance of the wrapped Int
.
newtype Score = Score Int
derive newtype instance Semiring Score
tenPoints :: Score
tenPoints = (Score 4) + (Score 6)
That derive
line replaced all this code:
-- No need to write this
instance Semiring Score where
zero = Score 0
add (Score a) (Score b) = Score (a + b)
mul (Score a) (Score b) = Score (a * b)
one = Score 1
Data.Newtype provides useful functions via deriving newtypes instances:
import Data.Newtype (Newtype, un)
newtype Address = Address String
derive instance Newtype Address _
printAddress :: Address -> Eff _ Unit
printAddress address = Console.log (un Address address)
For type classes without build-in support for deriving (such as Show
) and for types other than newtypes where newtype deriving cannot be used, you can derive from Generic
if the author of the type class library has implemented a generic version.
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
import Effect.Console (logShow)
derive instance Generic MyADT _
instance Show MyADT where
show = genericShow
-- logs `[Some,(Arbitrary 1),(Contents 2.0 "Three")]`
main = logShow [Some, Arbitrary 1, Contents 2.0 "Three"]
Here is a type class constraint Eq a
, separated from the rest of the type by a double arrow =>
:
threeAreEqual :: forall a. Eq a => a -> a -> a -> Boolean
threeAreEqual a1 a2 a3 = a1 == a2 && a2 == a3
This type says that we can call threeAreEqual
with any choice of type a
, as long as there is an Eq
instance available for a
.
Multiple constraints can be specified by using the =>
symbol multiple times:
showCompare :: forall a. Ord a => Show a => a -> a -> String
showCompare a1 a2
| a1 < a2 = show a1 <> " is less than " <> show a2
| a1 > a2 = show a1 <> " is greater than " <> show a2
| otherwise = show a1 <> " is equal to " <> show a2
The implementation of type class instances can depend on other type class instances. Those instances should be grouped in parentheses and separated by commas on the left-hand side of the =>
symbol:
instance (Show a, Show b) => Show (Either a b) where
...
There is a type class in Prim
called Warn
. When the compiler solves a Warn
constraint it will trivially solve the instance and print out the message as a user defined warning.
meaningOfLife :: Warn (Text "`meaningOfLife` result is hardcoded, for now.) => Int
meaningOfLife = 42
<$>
is the infix alias of the map
operator defined in the Functor type class.
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
The two following lines are equivalent:
map (\n -> n + 1) (Just 5)
(\n -> n + 1) <$> (Just 5)
To lift a function means to turn it into a function that works with functor-wrapped arguments. Applicative functors are functors that allow lifting of functions.
<*>
is the infix alias of the apply
operator defined in the Apply type class (that extends Functor
). <*>
is equivalent to |> andMap
in Elm (with andMap = Maybe.map2 (|>)
).
The Applicative type class extends the Apply
type class with a pure
function that takes a value and returns that value lifted into the applicative functor. With Maybe
, pure
is the same as Just
, and with Either
, pure
is the same as Right
, but it is recommended to use pure
in case of an eventual applicative functor change.
class Applicative f where
pure :: a -> f a
apply :: f (a -> b) -> f a -> f b -- "inherited" from the `Apply` type class
Applicative lets us perform N operations independently, then it aggregates the results for us.
You are in an applicative context when using Decoder
in Elm.
Let’s lift the function fullName
over a Maybe
:
import Prelude
import Data.Maybe
fullName :: String -> String -> String -> String
fullName first middle last = last <> ", " <> first <> " " <> middle
fullName "Phillip" "A" "Freeman" -- "Freeman, Phillip A"
fullName <$> Just "Phillip" <*> Just "A" <*> Just "Freeman" -- Just ("Freeman, Phillip A")
fullName <$> Just "Phillip" <*> Nothing <*> Just "Freeman" -- Nothing
Just like with Maybe
, if we lift fullName
over Either String String
, we get a unique error even if multiple errors occur:
import Test.Assert (assert)
import Data.Either (Either(..))
type Contact =
{ firstName :: String
, lastName :: String
, address :: Address
}
type Address =
{ street :: String
, city :: String
, country :: String
}
goodContact :: Contact
goodContact =
{ firstName: "John"
, lastName: "Doe"
, address:
{ street: "123 Main St."
, city: "Springfield"
, country: "USA"
}
}
badContact :: Contact
badContact = goodContact { firstName = "", lastName = "" }
nonEmptyEither :: String -> String -> Either String String
nonEmptyEither fieldName value
| value == "" = Left $ "Field '" <> fieldName <> "' cannot be empty"
| otherwise = Right value
validateContactEither :: Contact -> Either String Contact
validateContactEither c = { firstName: _, lastName: _, address: _ }
<$> nonEmptyEither "First Name" c.firstName
<*> nonEmptyEither "Last Name" c.lastName
-- lifting the `c.address` value into `Either` (we could also have used `Right c.address`)
<*> pure c.address
assert $ validateContactEither goodContact == Right goodContact
assert $ validateContactEither badContact == Left "Field 'First Name' cannot be empty"
To get an array of all the errors we can use the V
functor of Data.Validation.Semigroup
that it allows us to collect multiple errors using an arbitrary semigroup (such as Array String
in the example below).
import Data.Validation.Semigroup (V, invalid, isValid)
type ErrorMessages = Array String
nonEmptyV :: String -> String -> V ErrorMessages String
nonEmptyV fieldName value
| value == "" = invalid [ "Field '" <> fieldName <> "' cannot be empty" ]
| otherwise = pure value
validateContactV :: Contact -> V ErrorMessages Contact
validateContactV c = { firstName: _, lastName: _, address: _ }
<$> nonEmptyV "First Name" c.firstName
<*> nonEmptyV "Last Name" c.lastName
<*> pure c.address
assert $ isValid $ validateContactV goodContact
assert $ not isValid $ validateContactV badContact
assert $ validateContactV badContact ==
invalid
[ "Field 'First Name' cannot be empty"
, "Field 'Last Name' cannot be empty"
]
With the ado
keyword:
validateContactVAdo :: Contact -> V ErrorMessages Contact
validateContactVAdo c = ado
fistName <- nonEmptyV "First Name" c.firstName
lastName <- nonEmptyV "Last Name" c.lastName
address <- pure c.address
in { firstName, lastName, address }
>>=
is the infix alias of the bind
operator defined in the Bind type class (that extends Apply
). >>=
is equivalent to |> andThen
in Elm.
The Monad type class combines the operations of the Bind
and Applicative type classes. Therefore, Monad
instances represent type constructors which support both sequential composition and function lifting.
class Monad m where
bind :: m a -> (a -> m b) -> m b
So, to define a monad we need to define the map
, apply
, pure
and bind
operations:
data Box a =
Box a
instance Functor Box where
map :: forall a b. (a -> b) -> Box a -> Box b
map f (Box a) = Box (f a)
instance Apply Box where
apply :: forall a b. Box (a -> b) -> Box a -> Box b
apply (Box f) (Box a) = Box (f a)
instance Applicative Box where
pure :: forall a. a -> Box a
pure a = Box a
instance Bind Box where
bind :: forall a b. Box a -> (a -> Box b) -> Box b
bind (Box a) f = f a
instance Monad Box
Monadic operations operate sequentially not concurrently. That’s great when we have a dependency between the operations e.g. lookup user_id based on email then fetch the inbox based on the user_id. But for independent operations monadic calls are very inefficient as they are inherently sequential. Monads fail fast which makes them poor for form validation and similar use cases. Once something "fails" the operation aborts.
You are in a monadic context when using Task
in Elm.
The do
keyword is "syntactic sugar" for chained >>=
. It removes the need for indentations.
foo :: Box Unit
foo =
-- only call `(\x -> ...)` if `getMyInt` actually produces something
getMyInt >>= (\x ->
let y = x + 4
in toString y >>= (\z ->
print z
)
)
is the same as
foo :: Box Unit
foo = do
x <- getMyInt
let y = x + 4 -- `in` keyword not needed
z <- toString y
print z -- not `value <- computation` but just `computation`
The main
function of PureScript programs uses the Effect
monad for side effects:
import Effect.Random (random) -- random :: Effect Number
main :: Effect Unit
main =
(log "Below is a random number between 0.0 and 1.0:") >>= (\_ ->
random >>= (\n->
log $ show n
)
)
and is more readable using the do-notation:
main :: Effect Unit
main = do
log "Below is a random number between 0.0 and 1.0:"
n <- random
log $ show n
The above example works because the last line has a log
that returns Effect Unit
.
We can use the void
function to ignore the type wrapped by a Functor and replace it with Unit
:
void :: forall f a. Functor f => f a -> f Unit
That is useful when using the do-notation:
main :: Effect Unit
main = do
log "Generating random number..."
void random
Using asynchronous effects in PureScript is like using promises in JavaScript.
PureScript applications use the main
function in the context of the Effect
monad. To start the App
monad context from the Effect
context, we use the launchAff
function (or launchAff_
which is void $ launchAff
).
When we have an Effect-based computation that we want to run in some other monadic context, we can use liftEffect
from Effect.Class if the target monad has an instance for MonadEffect
:
class (Monad m) ⇐ MonadEffect m where
-- same as liftEffect :: forall a. Effect a -> m a
liftEffect :: Effect ~> m
Aff
has an instance for MonadEffect
, so we can lift Effect
-based computations (such as log
) into an Aff
monadic context:
import Prelude
import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, launchAff_)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect.Timer (setTimeout, clearTimeout)
main :: Effect Unit
main = launchAff_ do
timeoutID <- liftEffect $ setTimeout 1000 (log "This will run after 1 second")
delay (Milliseconds 1300.0)
liftEffect do
log "Now cancelling timeout"
clearTimeout timeoutID
We can run multiple computations concurrently with forkAff
. Then, we'll use joinFiber
to ensure all computations are finished before we do another computation.
import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, forkAff, joinFiber, launchAff_)
main :: Effect Unit
main = launchAff_ do
fiber1 <- forkAff do
liftEffect $ log "Fiber 1: Waiting for 1 second until completion."
delay $ Milliseconds 1000.0
liftEffect $ log "Fiber 1: Finished computation."
fiber2 <- forkAff do
liftEffect $ log "Fiber 2: Computation 1 (takes 300 ms)."
delay $ Milliseconds 300.0
liftEffect $ log "Fiber 2: Computation 2 (takes 300 ms)."
delay $ Milliseconds 300.0
liftEffect $ log "Fiber 2: Computation 3 (takes 500 ms)."
delay $ Milliseconds 500.0
liftEffect $ log "Fiber 2: Finished computation."
fiber3 <- forkAff do
liftEffect $ log "Fiber 3: Nothing to do. Just return immediately."
liftEffect $ log "Fiber 3: Finished computation."
joinFiber fiber1
liftEffect $ log "Fiber 1 has finished. Now joining on fiber 2"
joinFiber fiber2
liftEffect $ log "Fiber 3 has finished. Now joining on fiber 3"
joinFiber fiber3
liftEffect $ log "Fiber 3 has finished. All fibers have finished their computation."
If instead of forkAff
we used suspendAff
, then the fibers would not be run concurrently as soon as defined, but they would be suspended and ran sequentially one by one after their respective joinFiber
.
-- Purescript
module Tools where
import Prelude
-- find the greatest common divisor
gcd :: Int -> Int -> Int
gcd n m
| n == 0 = m
| m == 0 = n
| n > m = gcd (n - m) m
| otherwise = gcd (m - n) n
PureScript functions always get turned into Javascript functions of a single argument, so we need to apply its arguments one-by-one:
// JavaScript
import { gcd } from 'Tools'
gcd(15)(20)
A foreign module is just an ES module which is associated with a PureScript module.
- All the ES module exports must be of the form
export const name = value
orexport function name() { ... }
. - The PureScript module must have the same as the ES one but with the
.purs
extension. It contains the signatures of the exports.
// JavaScript (src/Interest.js)
export function calculateInterest(amount) {
return amount * 0.1
}
-- PureScript (src/Interest.purs)
module Interest where
foreign import calculateInterest :: Number -> Number
PureScript functions are curried by default, so Javascript functions of multiple arguments require special treatment.
// JavaScript
export function calculateInterest(amount, months) {
return amount * Math.exp(0.1, months)
}
-- PureScript
module Interest where
-- available for function arities from 0 to 10
import Data.Function.Uncurried (Fn2)
foreign import calculateInterest :: Fn2 Number Number Number
We can write a curried wrapper function in PureScript which will allow partial application:
calculateInterestCurried :: Number -> Number -> Number
calculateInterestCurried = runFn2 calculateInterest
An alternative is to use curried functions in the native module, using multiple nested functions, each with a single argument:
// JavaScript
export const calculateInterest = amount => months => amount * Math.exp(0.1, months)
This time, we can assign the curried function type directly:
-- PureScript
foreign import calculateInterest :: Number -> Number -> Number
Promises in JavaScript translate directly to asynchronous effects in PureScript with the help of Promise.Aff
.
In JavaScript, you need to wrap asynchronous functions in a PureScript Effect with a "thunk" () =>
so the function is not considered pure and is run every time:
// JavaScript
export const catBase64JS = text => fontSize =>
async () => {
const response = await fetch(`https://cataas.com/cat/says/${text}?fontSize=${fontSize}&fontColor=red`)
const array = await response.body.getReader().read()
return btoa(String.fromCharCode.apply(null, array.value))
}
Then in PureScript use the toAffE
function:
-- PureScript
import Promise.Aff (Promise, toAffE)
foreign import catBase64JS :: String -> Int -> Effect (Promise String)
catBase64 :: String -> Int -> Aff String
catBase64 text fontSize = toAffE $ catBase64JS text fontSize
It is important to sanitize data when working with values returned from Javascript functions using the FFI. For this we will use purescript-foreign-generic
.
import Data.Foreign
import Data.Foreign.Generic
import Data.Foreign.JSON
purescript-foreign-generic
has the following functions:
parseJSON :: String -> F Foreign
decodeJSON :: forall a. Decode a => String -> F a
F
is a type alias:
type F = Except (NonEmptyList ForeignError)
Note: The usage of the F
alias is now discouraged.
Except
is an monad for handling exceptions, much like Either
. We can convert a value in the F
monad into a value in the Either
monad by using the runExcept
function.
import Control.Monad.Except
runExcept (decodeJSON "\"Testing\"" :: F String)
-- Right "Testing"
runExcept (decodeJSON "true" :: F Boolean)
-- Right true
runExcept (decodeJSON "[1, 2, 3]" :: F (Array Int))
-- Right [1, 2, 3]
runExcept (decodeJSON "[1, 2, true]" :: F (Array Int))
-- Left (NonEmptyList (NonEmpty (ErrorAtIndex 2 (TypeMismatch "Int" "Boolean")) Nil))
Real-world JSON documents contain null and undefined values, so we need to be able to handle those too.
purescript-foreign-generic
defines a type constructors which solves this problem: NullOrUndefined
. It uses the Maybe
type constructor internally to represent missing values.
The module also provides a function unNullOrUndefined
to unwrap the inner value. We can lift the appropriate function over the decodeJSON
action to parse JSON documents which permit null values:
import Data.Foreign.NullOrUndefined
runExcept (unNullOrUndefined <$> decodeJSON "42" :: F (NullOrUndefined Int))
-- Right (Just 42)
runExcept (unNullOrUndefined <$> decodeJSON "null" :: F (NullOrUndefined Int))
-- Right Nothing
To parse arrays of integers where each element might be null, we can lift the function map unNullOrUndefined
over the decodeJSON
action:
runExcept (map unNullOrUndefined <$> decodeJSON "[1, 2, null]" :: F (Array (NullOrUndefined Int)))
-- Right [(Just 1),(Just 2),Nothing]
In The state of PureScript 2023 survey results, at page 24, you can see a chart of the most used front-end frameworks:
-
Halogen is by far the most used front-end framework for PureScript.
- Not using The Elm Architecture ("TEA").
- You can create components if you’re into that stuff. There is purescript-halogen-store for global state management.
- Does not have a router. You will have to use purescript-routing or purescript-routing-duplex.
- A bit slower than Elm.
- About twice heavier than Elm with brotli compression for a real world app.
-
Elmish, as its name suggests, uses Elm ideas:
- (loosely) follows TEA principles, implemented as a thin layer on top of React.
- Has routing capabilities.
- Bloated with React 17 (I couldn’t figure out how to get it to work with Preact).
- Seems abandoned as the package maintainer no longer has the motivation to update it to React 18 and above.
-
Flame is a relatively new framework inspired by Elm:
- The Flame architecture is inspired by TEA.
- Does not have a router. You will have to use purescript-routing or purescript-routing-duplex.
- Performance is comparable to Elm.
- Server Side Rendering is supported.
This repo contains a minimal Flame example with a counter increment/decrement buttons, random number generation, synchronous and asynchronous FFI calls, subscription and decoding of a JSON object.
npm i
- When using PureScript IDE for VS code the project is built every time you save a file. There is no need for a special Vite plugin.
output/Main/index.js
is simply imported in the JavasScript entry file. - Terser is used for better compression results.
We only covered the basics of PureScript here. If you want to learn more, check out the following resources:
- Official documentation of course.
- PureScript: Jordan's Reference is a must.
- PureScript By Example is a free online book containing several practical projects for PureScript beginners.
- Functional Programming Made Easier by Charles Scalfani is a great book to learn PureScript.
- PureScript Discourse forum
- PureScript Discord chat