Purescript for Elm developers
- Data Types
- Lists
- Records
- Tuples
- Imports
- Default Imports
- Type Signatures
- Common Packages
- Common Functions
- Typeclasses
- Useful Things
- Nice Things in Purescript
- Stuff you'll often see
- Bower and Package Management
- Elm Architecture
Data types
-- Elm
type Direction = Up | Down
type alias Time = Int
-- Purescript
data Direction = Up | Down
type Time = Int
Lists
Constructing lists.
-- Elm
list = [1,2,3]
newList = 5 :: list
-- In Purescript, the quickest way to create
-- a list is from a Foldable structure
-- (an Array in this case)
list = List.fromFoldable [1,2,3]
newList = 5 : list
Pattern matching on lists is almost the same.
-- Elm
case xs of
[] -> ...
x :: rest -> ...
-- Purescript
case xs of
Nil -> ...
x : rest -> ...
Be careful! [1,2,3]
is syntactic sugar for List Int
in Elm but Array Int
in Purescript.
Should you use List
or Array
in Purescript? They perform differently but it shouldn't matter in most cases. Use whatever you like more.
Records
Define a record.
-- Elm
type alias Person = { name : String, age : Int }
-- This will also define a function Person in Elm
-- not so in Purescript
-- Purescript
type Person = { name :: String, age :: Int }
Create a new record.
-- Elm
p = { name = "Bob", age = 30 }
p = Person "bob" 30
--Purescript
p = { name: "Bob", age: 30 }
Edit a record.
-- Elm
edited = { p | name = "Alice" }
-- Purescript
edited = p { name = "Alice" }
Accessing properties is the same in both languages.
p.name
Destructuring is the same.
-- Elm
personName : Person -> String
personName { name } = name
-- Purescript
personName :: Person -> String
personName { name } = name
Aliases for function arguments. In both examples, p
still references the whole record.
-- Elm
bumpAge : Person -> Person
bumpAge { age } as p =
{ p | age = age + 1 }
-- Purescript
bumpAge :: Person -> Person
bumpAge p@{ age } =
p { age = age + 1 }
Check out Updating records for more info.
Tuples
-- Elm
coords = (10, 20)
(x, y) = coords
-- Purescript
import Data.Tuple
coords = Tuple 10 20
Tuple x y = coords
Tuples are part of the language in Elm, while they're just a data type in Purescript. This is not that big of a deal, use records when possible.
Imports
In Elm, imports are qualified by default while in Purescript everything from the module is imported.
-- Elm
import Set
a : Set.Set Int
a = Set.empty
-- Purescript
import Data.Set
a :: Set Int
a = empty -- note that empty is coming from the module (Data.Set)
I actually like qualified imports more, so I often alias my imports. This works the same in both languages.
import Data.Set as Set
a :: Set.Set Int
a = Set.empty
It's a little bit cumbersome if you want to have data types and constructors in scope though
import Data.Maybe (Maybe(..))
import Data.Maybe as Maybe
a :: Maybe Int -- note that this is not Maybe.Maybe Int
a = Just 5
-- this does not work in Purescript, wish it did :(
-- that is, you can't import stuff and alias the module
-- in the same import statement
import Data.Maybe as Maybe exposing (Maybe(..))
Importing specific things is the same.
-- Elm
import Set exposing (empty)
-- Purescript
import Data.Set (empty)
-- Set is not in scope in both cases
Default imports
-- In Elm, the Basics package is imported by default
-- it's like every file has this implicit declaration
import Basics exposing (..)
-- In Purescript nothing is imported by default
-- so you have to explicitly import the Prelude
import Prelude
The Prelude
isn't imported by the compiler automatically so that it's easier for a team to decide to use something different than the stock one. I get why this choice was made, but it's one more thing to keep in mind that you don't necessarily want to deal with. ๐
Also, the default Prelude
is rather lightweight, meaning it does not import anything immediately useful (compared to the default imports in Elm). I find myself almost always importing this stuff at the very least.
import Data.Maybe
import Data.Either
import Data.Array as Array
import Data.List (List(..))
import Data.List as List
import Control.Monad.Eff.Console as Console
import Debug.Trace as Debug
This is not a critic, just me being lazy. I guess a more relaxed Prelude
will be released and maintained at some point, but nothing stops you from defining your own. By not having a default, you effectively solve the Prelude
hell that there is in Haskell, where unsafe functions and obscure choices made 20 years ago are still around to this day for backwards compatibility.
(Full and up to date list of modules automatically imported by Elm can be found here. It's a bit more than just the Basics
package)
Type signatures
Type signatures are separated with double colons
sum : Int -> Int -> Int -- Elm
sum :: Int -> Int -> Int -- Purescript
You have to be explicit about the type variables you are using.
map : (a -> b) -> Maybe a -> Maybe b -- Elm
map :: forall a b. (a -> b) -> Maybe a -> Maybe b -- Purescript
What is this forall
thing? You might want to read up some stuff about it. This issue is a good start purescript/purescript#766.
There are minor differences with signatures for extensible records.
-- Elm
getAge : { a | age : Int } -> Int
getAge { age } = age
-- Purescript
getAge :: forall a. { age :: Int | a } -> Int
getAge { age } = age
Common packages
Elm | Purescript | Notes |
---|---|---|
Array | Data.Array | |
Dict | Data.Map | |
List | Data.List | |
Maybe | Data.Maybe | |
Result | Data.Either | Err is Left and Ok is Right |
Set | Data.Set | |
String | Data.String | |
Debug | Debug.Trace | Trace.spy is the closest thing to Debug.log |
Common Functions
Elm | Purescript |
---|---|
identity | id |
always | const |
toString | show |
>> |
>>> |
<< |
<<< |
|> | # |
<| | $ |
Maybe.map
?
Wait, why there's no Because you can use typeclasses!
Typeclasses instead of monomorphic code
I'd say the major difference between the two languages is the level of abstraction they let you work with.
People like to whine about Elm not having type classes, lacking Higher Kinded Types, about it being dumb and stupid... there's a reason why Elm is the way it is. One of the benefits/consequences of not having typeclasses/HKT is that code is going to be simpler (because it's monomorphic) and as a result, the language is much easier to learn for beginners. That's one of the great qualities of Elm and I wish people could just accept not every language is meant to be Haskell.
With that out of the way, Elm code is surely simple, but also quite boilerplate-y. Why is that? Well, because there are very little means of abstractions so some things have to be repeated over and over. Have you ever noticed that map
(for example) is defined multiple times in different modules?
map : (a -> b) -> Maybe a -> Maybe b
map : (a -> b) -> Result x a -> Result x b
map : (a -> b) -> List a -> List b
The type signatures do look very similar indeed! Can this be abstracted in some form? Well yes, map
is in fact the core operation of the Functor
typeclass! So exciting.
In Purescript, you will rarely find type signatures this specific. The Data.Maybe
module in Purescript does not expose a map
function. Instead, an instance for Functor
is provided for the Maybe a
type.
I don't want to get into typeclasses too much, just know that, in practice, you have a single map
operation in Purescript that works for all Functor
s.
-- one map to rule them all
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
You will see different instances of this typeclass for suitable types. Implementation wise, they will look very similar to the monomorphic counterpart in Elm.
The other piece of the puzzle are Higher Kinded Types. You should have noticed that the type signature for map
has f a
and f b
in there. These are not expressible in Elm but are very useful to make our code more abstract and reusable. If we take Maybe
as an example, f a
and f b
become respectively Maybe a
and Maybe b
, so the type signature is exactly the same we have in Elm!
This is like typeclasses 101 and I realize that without examples this is not very useful. Do your own reading!
How to do some useful things
-- Elm
Just 5
|> Maybe.map (\n -> n + 10)
|> Maybe.withDefault 50
-- Purescript
Just 5
# map (\n -> n + 10)
# maybe 50 id
Just 5
# map (_ + 10) -- cool stuff
# maybe 50 id
maybe 50 (_ + 10) (Just 5)
Nice things unique to Purescript
The Language Reference is a good starting point. The main features to look out for are:
_
can be used in functions when you don't want to give the parameter a name. It's like anonymous functions on steroids.- Pattern matching can be done at the function level. Make sure you provide a type signature. Take a look at this example to see why. Although it is possible to have partial functions in Purescript, you should obviously strive to write total functions (reminder, you can only define total functions in Elm).
- Guards are pretty cool and can tidy up your code quite a bit. Read more about Guards.
- Typed holes are just awesome. In Elm you can use type bombs to achieve similar results, but holes are way more powerful. I suggest you take a look at the tutorial Generating collections of random numbers in PureScript to understand how to use them (read it!).
Stuff you'll often see in Purescript
Or, what's with all the dollars and weird operators?!
The $
operator is just function application. It serves the same purpose of the reverse pipe <|
in Elm, that is, avoid parenthesis.
The forward pipe is defined as #
. They're exactly the same thing.
If you miss the pipe operators you can easily define them yourself like this:
import Data.Function (apply, applyFlipped)
infixr 0 apply as <|
infixl 1 applyFlipped as |>
<$>
is alias for map
but in infix position, meaning that the two are equivalent
map (\n -> n + 10) (Just 5)
(\n -> n + 10) <$> (Just 5)
I still prefer using map
but the second version is probably more idiomatic? I don't know, use whatever you like ๐
Now the following might be a little too much if you haven't played around with this stuff, so I suggest you read about common typeclasses (Functor
, Applicative
and Monad
) and learn a bit of theory so that you can figure out these things for yourself.
<*>
is the apply
operation for Applicative
. The intuition is that <*>
is somewhat equivalent to andMap
in Elm.
>>=
is the bind
operation for Monad
. The intuition is that >>=
is somewhat equivalent to andThen
in Elm.
do
notation is used to make the code more readable. It's just syntactic sugar. If you ever wrote a slightly complex decoder in Elm, you'd know it's not a piece of cake to pipe your way to the result. do
notation helps when you are in an Applicative context (like a Decoder
) or a Monadic one (like Task
), so that the code you write is more expressive.
If this doesn't make sense, don't worry, you'll figure it out with some practice.
Bower and why you should not care
Yeah Purescript still uses Bower, but honestly, it's not that big of a deal. It actually works quite well and I don't see it as being any worse/better than npm or another package manager. It just downloads stuff, it's fast and has a global cache, that's all I care about.
If you really don't want to use it, check out psc-package and package-sets. They are more akin to the way Stackage in the Haskell world works. Basically a package-set is a list of package versions that are guaranteed to work well together, so as long as you stick to using the packages defined in a specific set, you're good to go because you won't have conflicts among your dependencies. I haven't tried it yet, and I think it is still sort of an experimental thing, but might become standard in the future.
This post has a lot of good info as well: Setting up PureScript in March 2018
Don't feel bad for using Bower. It's fine.
Elm Architecture
It's possible to get a pretty solid Elmish architecture in Purescript today! The library I like the most is purescript-spork. You should take a look at the examples (ported from https://guide.elm-lang.org/) and see for yourself.
Samples applications are available in the elm-architecture folder of this repo.