Comparing Elm and PureScript Features
Comparing Elm and PureScript Features
1
Automatically Imported Modules
The only way to tell was to wait until the compiler complained.
2
NO elm-format
3
NO elm-format (cont.)
f x =
let y = x
+ 10 -- the indent has to be deep enough
-- so it's indented past the `y`
-- NOT the `let`
4
Primitive Types
5
Unit Type
Elm has the Unit Type () which has ONE AND ONLY ONE
Inhabitant called ().
PureScript has Unit Type called Unit which has ONE AND ONLY
ONE Inhabitant, called unit.
6
Unit Type (cont.)
7
Unit Type (cont.)
"use strict";
[Link] = {};
8
Void Type
Elm has the Void Type Never which has ZERO Inhabitants since
you cannot construct something of type Never.
PureScript has the Void Type Void which has ZERO Inhabitants
since you cannot construct something of type Void.
9
Void Type (cont.)
In Elm:
Here, the only way to create a Never is to give the Data Constructor
JustOneMore something of type Never which can only be created by using
JustOneMore which needs a Never and on and on forever.
JustOneMore (JustOneMore (JustOneMore (JustOneMore ...)))
In PureScript:
newtype Void = Void Void
They also do not export the Data Constructor, also called Void.
(more about newtype later)
11
Void Type (cont.)
There are times where you want to handle something of type Void:
rightOnly :: forall t . Either Void t -> t -- more on forall later
rightOnly (Left v) = ????????????
rightOnly (Right t) = t
Here the Left case is impossible to write. We need to take the Void,
v, and return something of type t.
Basically, it says that IF YOU CAN GIVE ME A VOID, I'LL GIVE YOU
SOMETHING OF ANY TYPE.
13
Absurd (cont.)
An implementation of absurd:
Since this function can never be called, all it has to do it satisfy the Type Checker.
It basically unwraps the first of an Infinite number of Voids and then Infinitely
recurses.
This keeps the Type Checker quite happy since absurd takes a Void which it gets
from x.
14
Absurd (cont.)
Here v is a Void and that's exactly what absurd wants. And since it
turns that into ANY TYPE, it unifies with t.
And we never have to worry that it'll be called because no one can
ever construct a Left Void.
15
Some more formatting examples
let s = 10 -- ALL variables are aligned under the `s`
t = "This is a really long string"
<> "another long string" -- the easiest way by just using tab (more about <> later)
u = "You can also indent like this"
<> "If you like this better" -- here you have to indent the first line
v = "Not my favorite way"
<> "to indent, but still valid" -- here this line is indented by ONLY 1
w = 20 in
let s = 10 in
let t = 20
in
let
u = 30
in
16
Syntactical Differences - Apostrophe/Prime
Elm can no longer have a prime in the name. They removed it from the
Language 2 versions back.
18
Syntactical Differences - Apply
f x $ g y
g y # f x
19
Syntactical Differences - Type Signatures
20
Syntactical Differences - Compose
In PureScript, there are two ways, data to define a data type or newtype to define a new name to
an existing type (this is NOT an alias).
Elm:
PureScript:
22
Type differences - newtype
newtype must take ONLY 1 constructor and that constructor must take ONLY
1 argument.
This makes it impossible to pass Pixels to a function that wants Inches and
vice versa even though they both contain Number.
BTW, using these newtypes doesn't have any more overhead at runtime than
just using Number. The real benefit is that the compiler will treat them as
different types.
23
Type differences - Aliases
Type aliases are just a different name for an existing type. Many
times, these are used to make reading types easier. In Elm:
type alias MiddleName = String -- Elm
becomes:
type MiddleName = String -- PureScript
s2 :: String
s2 = "This is a string\
\ on two lines" -- everything before the \ is ignored
s3 :: String
s3 = """
This is a
string on
three lines
"""
s3' :: String
s3' = "\nThis is a\nstring on\nthree lines\n" -- same as `s3`
26
String Literals with Regex
Triple quotes makes typing Regex strings much easier since you
don't have to escape backslashes.
regex ".+@.+\\..+" noFlags
versus:
regex """.+@.+\..+""" noFlags
27
Boolean differences
• Elm
• PureScript
28
Lists and Arrays
PureScript Lists are linked lists just like Elm. They can be defined
by importing [Link].
29
Lists and Arrays - Literals
Arrays literals can be defined as:
[1, 2, 3]
["abc", "xyz"]
30
Lists and Arrays - Literals (cont.)
You can also use fromFoldable from [Link] to convert an
Array literal to a List:
import [Link] (fromFoldable)
31
Let and Where
In Elm, you can only introduce local values in a let block. The let
block must come at the BEGINNING of a declaration.
32
Let and Where (cont.)
let vs where:
f :: Int -> Int
f x = let multiplier = 10 in
multiplier * x
N.B. you can create alternative versions of a function or value and name
it identically except for adding a prime (single quote) to distinguish it.
33
Let and Where (cont.)
Whether to use let or where is more about personal taste.
If you use let, then it requires the reader to first understand the
nuance of the function, before understanding the concept.
Using where, starts with the concept of what the function does and
then burdens the reader with the details as an after thought.
34
Guards
Guards allow you to place a condition on a function's computed value by using a vertical bar, |.
35
Guards (cont.)
Most of the time, guards are easier to read than if logic. Compare the difference
between a function with if-then-else and it's equivalent using guards:
36
Guards in Case Statement
Guards can also be used in case statements:
noBiggerThan10 :: Maybe Int -> Int
noBiggerThan10 x = case x of
Just x | x > 10 -> 10
| otherwise -> x
Nothing -> 0
37
Pattern Matching differences
Elm MUST use a case statement to pattern match.
{-| `bool` performs case analysis for the `Boolean` data type, like an `if` statement.
-}
bool : Bool -> a -> a -> a
bool b x y =
case b of
True ->
x
False ->
y
38
Pattern Matching differences (cont.)
PureScript can specify different versions of the function with
different patterns.
-- | `bool` performs case analysis for the `Boolean` data type, like an `if` statement.
bool :: forall a. Boolean -> a -> a -> a
bool true x _ = x
bool false _ y = y
39
Pattern Matching differences (cont.)
Elm must be total whereas PureScript can be partial, with a bit of extra cruft.
If bool is called with false 1 2, then the following Runtime Error will be
produced:
bool false 1 2
Failed pattern match
40
Pattern Matching differences (cont.)
In Elm, patterns can be named with an as and a name. Here tup is bound to the tuple (x, y):
In PureScript, patterns can be named with a name and an @ symbol. Here the pattern is matched
with [x, y] and the full array is bound to the name arr.
41
Pattern Matching - Lists
Any arbitrary length List can be pattern matched. Unlike Elm, Lists in
PureScript are defined as a Sum Type (Union Type in Elm).
-- this definition can be found in [Link]
data List a = Nil | Cons a (List a)
Which means that they must be pattern matched using Nil and the
Cons constructor.
length :: forall a. List a -> Int
length Nil = 0
length (Cons x xs) = 1 + length xs
42
Pattern Matching - Lists (cont.)
You can also pattern match with the Cons operator:
length :: forall a. List a -> Int
length Nil = 0
length (x : xs) = 1 + length xs
-- length (Cons x xs) = 1 + length xs
43
Pattern Matching - Arrays
Arrays can ONLY be pattern matched with a known length. So you cannot write a
length function in PureScript.
45
Pattern Matching - Records
type Address = { street :: String, city :: String }
-- simple
isAlice :: Person -> Boolean
isAlice { name: "Alice" } = true
isAlice _ = false
-- nested
livesInLA :: Person -> Boolean
livesInLA { address: { city: "Los Angeles" } } = true
livesInLA _ = false
46
Pattern Matching - Records (cont.)
Record Puns can be used to destruct or construct records.
data Point = Point {x :: Int, y :: Int}
-- Destruct
showPoint :: Point -> String
showPoint (Point { x, y }) = "(" <> x <> ", " <> y <> ")"
origin :: Point
origin = Point { x, y } -- Construct
where
x = 0.0
y = 0.0
47
Documentation comments
Comments that start with a pipe character, |, are considered
documentation, and will appear in the output of tools like psc-docs
and Pursuit.
-- | `multiply` does a multiplication
-- This line will NOT be included in docs
-- | This line will thanks to the PIPE character
multiply :: Number -> Number -> Number
multiply x y = x * y
48
Records
Records in Elm and PureScript are similar, e.g. both support Row
Polymorphism.
49
Records - Alias vs Constructor
Records can be defined as simple aliases:
type RA = {a :: String}
ra :: RA
ra = {a: "aaa"}
Or with constructors:
newtype R = R {a :: String}
r :: R
r = R {a: "aaa"}
50
Records - Alias vs Constructor (cont.)
newtype R = R {a :: String}
51
Records - Row Polymorphism
Now consider another record.
type Restaurant =
{ name :: String
, address :: String
}
And imagine we want to write a function to get the name of Person and Restaurant.
52
Records - Row Polymorphism (cont.)
But these 2 functions are doing practically the same thing.
Wouldn't it be nice to have a single function that could do both?
53
Records - Row Polymorphism (cont.)
We need to be a bit more specific than a since a is any type NOT
just records.
54
Records - Row Polymorphism (cont.)
In Elm we write
getName : {a | name : String} -> String
getName record = [Link]
Or we can write
getName : {a | name : String} -> String
getName {name} = name
55
Records - Row Polymorphism (cont.)
In PureScript we write
Or we can write
Or with renaming
56
Records - Accessors
To access a field in a row of a record in Elm
.name record
In PureScript
_.name record -- evaluates to `[Link]`
57
Records - Updates
To update a record in Elm
{record | name = "Elm"}
In PureScript
record {name = "PureScript"}
N.B. Updates use the EQUAL SIGN and NOT the colon. This is the
ONLY time the EQUAL SIGN is used in Records.
58
Records - Nested Updates
type SomeRecord =
{ val :: Int
, rec ::
{ val2 :: Int
, name :: String
}
}
r :: SomeRecord
r =
{ val: -10
, rec:
{ val2: 20
, name: "Bob"
}
}
59
Records - Nested Updates (cont.)
r :: SomeRecord
r =
{ val: -10
, rec:
{ val2: 20
, name: "Bob"
}
}
60
Records - Nested Updates (cont.)
r :: SomeRecord
r =
{ val: -10
, rec:
{ val2: 20
, name: "Bob"
}
}
In PureScript
r {rec {name = "Fred"}} -- all in one step
61
Records - Wildcards
{name: _, age: _}
is equivalent to
\name age -> {name: name, age: age}
-- or more concise
\name age -> {name, age}
Also, note that since we are CREATING a record, it uses the colon
syntax.
62
Records - Wildcards (cont.)
{name: _, age: _}
63
Records - Wildcards (cont.)
_ { name = "Victor" }
is equivalent to
\rec -> rec { name = "Victor" }
Note that this is UPDATING a record since it's using the equals
syntax.
64
Records - Wildcards (cont.)
_ { name = "Victor" }
We can use this function to change the name in any record with a
name field:
record # _ { name = "Victor" }
65
Records - Setters and Getters
In Elm, we have Getters for Records but no Setters:
.name -- Getter
66
Records - Extensible
Elm:
PureScript:
type Nameable r =
{ firstName :: String
, lastName :: String
| r
}
67
Records - Extensible (cont.)
Extensible records can be extended in the following ways:
type Nameable r =
{ firstName :: String
, lastName :: String
| r
}
type Person = Nameable (age :: Int)
type FictionalPerson = Nameable (age :: Int, wingspan :: Int)
Notice how parenthesis are used instead of the record syntax like in Elm.
This makes more sense since r isn't replaced with a record but with a Row Type.
68
Records - Syntactical Sugar
A Record:
type Person =
{ name :: String
, age :: Int
}
type Person =
Record
( name :: String -- N.B. Row Type
, age :: Int
)
69
Binary Operators
In Elm, the freedom to create your own Binary Operators was removed in version
0.19, but in PureScript this powerful feature is supported.
But, you can ONLY create an operator after you've created a named function for
the operation. For example:
l1 :: List Int
l1 = [1, 2] # fromFoldable -- (1 : 2 : Nil)
l2 :: List Int
l2 = [3, 4] # fromFoldable -- (3 : 4 : Nil)
l3 :: List Int
l3 = l1 <> l2 -- (1 : 2 : 3 : 4 : Nil)
72
Binary Operators - Associativity (cont.)
Right associative, i.e. infixr, implies right parenthesis
l1 <> l2 <> l3 <> l4
-- is equivalent to
(l1 <> (l2 <> (l3 <> l4)))
73
Binary Operators - Associativity (cont.)
Non-associative, i.e. infix, implies NO parenthesis. This means
that repeated use of this operator (or with other non-associative
operators) may NOT be used without EXPLICIT parenthesis. For
example:
true == true == true -- will NOT compile
(true == true) == true -- will compile
74
Binary Operators - Precedence
In Prelude, * is precedence 7 (higher) and + is precedence 6
(lower).
is equivalent to:
(2 * 3) + 4
75
Binary Operators - Precedence (cont.)
infixr 5 append as <>
76
Binary Operators - Fixity
Reminder: infix stands for infixed operator, i.e. one that is in
between two values, e.g.:
l1 <> l2
77
Binary Operators - Fixity (cont.)
It is possible to use operators in prefix mode:
(<>) l1 l2
78
Binary Operators - Sections
Imagine you have a function that halves a number.
In Elm:
\x -> x / 2
79
Binary Operators - Sections (cont.)
And to do the inverse.
In Elm:
\x -> 2 / x
PureScript:
(2 / _)
80
Binary Operators - Sections (cont.)
The PureScript approach is much more readable than a lambda
expression.
versus
(_ - 2) -- PureScript
81
Tuples
Unfortunately, there is no special syntax for Tuples as in Elm and
Haskell.
82
Tuples (cont.)
We use them just like any other type. In Elm:
fst : (a, b) -> a
fst (x, _) = x
83
Case statements
Case statements have a few more options in PureScript, e.g.
guards, wildcards and multiple case values.
84
Case statements (cont.)
In PureScript, we don't have the nice tuple syntax, so we can do
multiple values by just separating the values with a comma:
f :: Maybe Bool -> Maybe Bool -> String
f x y =
case x, y of
Just true, Just true -> "Both are true"
Just false, Just false -> "Both are false"
_ -> "They are different"
85
Case statements - Guards
You can use guards, instead of if, then and else, in your case
statements. This makes the logic easier to read:
f :: Either Int Unit -> String
f x = case x of
Left x | x == 0 -> "Left zero"
| x < 0 -> "Left negative"
| otherwise -> "Left positive"
Right _ -> "Right"
86
Case statements - Wildcards
In Elm, if you do a case in a lambda expression, then you MUST make a
variable to use in the case.
This isn't just verbose, but burdens us with naming yet another variable. One
of the benefits of a lambda is NOT having to name the temporary function.
Well, in PureScript you don't have to name the case variable if it's involved in
a lambda.
87
Case statements - Wildcards (cont.)
In Elm:
getCount
|> \count -> case count of
0 -> "Zero"
1 -> "One"
_ -> "More"
In PureScript:
getCount # case _ of
0 -> "Zero"
1 -> "One"
_ -> "More"
88
Type Holes
Many times we cannot figure out what type something value or
function is. In Elm, you can't ask the compiler help directly.
You can explicitly make something an Int and then investigate the
compiler error message to hopefully discern the appropriate type.
In PureScript, there are things called Type Holes which are named
variables that start with a question mark, For example:
?whatGoesHere or ?help.
89
Type Holes (cont.)
Imagine you have the following code:
a :: Array Int
a = ?whatGoesHere (_ + 1) [1,2,3]
but you cannot remember what function you want to call. The
compiler will now give you an error with the type that was inferred:
90
Type Holes (cont.)
Hole 'whatGoesHere' has the inferred type
[Link].liftA1 :: forall f a b. Applicative f => (... -> ...) -> ... -> ...
[Link].liftM1 :: forall m a b. Monad m => (... -> ...) -> ... -> ...
[Link] :: forall a b. Ord b => (... -> ...) -> ... -> ...
[Link] :: forall a b f. Functor f => (... -> ...) -> ... -> ...
[Link] :: forall i f a b. FunctorWithIndex i f => (... -> ...) -> ... -> ...
[Link] :: forall m. Monoid m => m
[Link] :: forall a b. a -> b
in value declaration a
91
Type Holes (cont.)
So now we can replace ?whatGoesHere with the map function:
a :: Array Int
a = map (_ + 1) [1,2,3]
92
Type Holes (cont.)
You can also use Type Holes to help just with types:
b :: ?help
b = map (_ + 1) [1,2,3]
Compiler error:
Hole 'help' has the inferred type
Array Int
in value declaration b
93
Type Holes (cont.)
You can also use an underscore (for types ONLY, doesn't work for functions). But
it's less useful and only produces a compiler WARNING.
c :: _
c = map (_ + 1) [1,2,3]
Compiler warning:
Array Int
in value declaration c
94
Type Holes (cont.)
The biggest problem with type holes in PureScript is that your
program MUST compile before it'll give you an error on type holes.
The best you can do is comment out as much code as you can to
get the code to compiler, sans the type hole which always
produces an error.
95
Universal Quantifier
Many of the examples in this presentation have used the forall in
Polymorphic Function definitions.
96
Universal Quantifier (cont.)
For more on why the language designers decided to go this route,
see the Github Discussion where Fresheyeball, the author of many
Elm packages asks whether forall could be implicit like in Elm
and Haskell.
([Link]
97
Universal Quantifier (cont.)
So in Elm, we type:
identity : a -> a
identity x = x
98
Universal Quantifier (cont.)
Universal Quantifiers come from Predicate Logic and use the
familiar symbol, ∀. This makes reading universal qualification
much easier than forall.
99
Universal Quantifier (cont.)
In Atom, you edit your [Link] file by choosing Snippets...
from the Atom menu and adding the following:
'.[Link]':
'Forall':
'prefix': 'fa'
'body': '∀'
100
Universal Quantifier (cont.)
You can type the ∀ character on the Mac by first going into the
Character Viewer via Ctrl-Cmd-Space. Then go to the Math Symbols
section in the Left Pane. Find the ∀ character and double-click on it
to insert it into the current program.
You will need this to enter the ∀ in Atom's (or your editor's)
configuration.
101
Universal Quantifier (cont.)
102
Universal Quantifier (cont.)
The following is easier to read and less to type:
fst :: ∀ a b. Tuple a b -> a
fst (Tuple x _) = x
103
Functors
In Elm, they don't have a formal concept of Functors. But they have
lots of Functors, e.g. List, Maybe, Result, Decoder, Encoder, etc.
But because of this, you have to use the appropriate map function
from the corresponding package, e.g. [Link] on lists.
104
Functors (cont.)
In PureScript, you don't have to worry about the types since the
compiler will figure the types out for you.
105
Functors (cont.)
To map (_ + 1) over this we must map over the List and then over the Maybe. In Elm:
PureScript:
Here <$> is the Operator for map, which, in this case, is mapping over the List with the
function map (_ + 1).
106
Functors (cont.)
The <$> operator is a map function. The compiler figures out which
map to call based on the types.
The operator <#> (flip map) is analogous to # in the same way, i.e.
the function comes AFTER.
map (_ + 1) <$> lms == lms <#> map (_ + 1)
107
Functors (cont.)
lms <#> map (_ + 1)
I start with lms, a List, that I need to map over with <#>, which
contains Maybe Ints, which I need to map over to add 1.
108
Product and Sum Types
110
Product and Sum Types (cont.)
Now for a Sum Type S:
ThingBool True
ThingBool False
ThingVariant This
ThingVariant That
ThingVariant TheOther
Note there are 2 inhabitants of ThingBool Boolean and 3 inhabitants of ThingVariant Variant.
There are 2 + 3 = 5 inhabitants of S. Hence, Sum Type.
111