PureScript is a small, modern pure functional programming language that compiles to JavaScript. It is written in and inspired by Haskell, with an expressive static type system designed for natural, effortless interaction with existing JavaScript programs. In this gentle introduction to the PureScript programming language, we will examine its type system, syntax, idioms, and current best practices.
- Image credit: http://crowactive.com/wp-content/uploads/2010/05/crow6sm.jpg
- PureScript is a small functional programming language that compiles to -- and, more importantly, easily interfaces with -- JavaScript.
- We'll take a quick tour of the language and see what makes it special. Then, we'll see how we can start using it and learning about it in depth.
- Functional programming languages are concerned with values. These are the values we use in PureScript. For those of you with a Haskell background, most of the syntax and many of the concepts will be pretty familiar, so bear with me. A notable difference to keep in mind is that PureScript is strict, whereas Haskell is lazy.
- We have a number (specifically a double), a string, a Boolean, an array, a record, and a function. These values are internally represented and exposed through FFI as the associated JavaScript value.
- The double-colon syntax specifies the type of each value. We'll be including explicit type signatures throughout these slides, but none of them are needed -- the PureScript compiler can almost always infer the type of each value. Though, it is convention to include explicit type signatures with each top-level declaration.
- Defining functions in PureScript is very easy. The sum function expects two parameters,
a
andb
, and returns their sum. - When a PureScript function is given fewer arguments than it's expecting, it'll return a function that expects the remaining arguments. This is called auto-currying. The successor function takes advantage of this by being defined in terms of sum.
- And of course, you can fully apply a function as we do in the definitions of
sumSuccessors
andseven
.
- Functions don't have to be defined in terms of concrete types. The type of the return value of some functions is determined by the type of its parameters. This is called parametric polymorphism.
- The
identity
function simply returns whatever it's given. - The
const
function returns its first parameter regardless of the value or type of its second parameter. $
is an infix function, which means it's written between its two arguments when it's applied. Theinfixr
declaration below declares that it associates to the right (r) with a very low precedence (0).
- Records are a free-form data structure very much like JavaScript objects. Unlike Haskell, we do not automatically create functions for accessing a record's fields. PureScript has a syntactic form for record field access using a dot.
hank
here hasname
andbirth year
fields, and mylaptop
hasname
andmanufacturing date
fields. The order of the fields is inconsequential, just like JavaScript objects.- We can write a function that can say hello to either of these records because they have a structural similarity. We read the type signature of
greet
as "a function that takes a record with a name field and zero or more other fields calledr
, then produces a string". We callr
a "row of fields", and this is called row polymorphism, one of PureScript's most notable features. In PureScript, type variables liker
are always explicitly universally quantified, unlike in Haskell. hankJr
is defined in terms ofhank
, using a record update. This does not mutatehank
. It can be thought of as an object with just abirth year
own-property that hashank
as its prototype.
- In addition to the primitive data types, PureScript allows you to define your own data types. The
data
keyword introduces an Algebraic Data Type (or ADT). - The Ordering ADT has exactly three possible values (or inhabitants): Less, Equal, and Greater. We can use those values just as we do values of any other type. Here, the
compare
function returns an Ordering by returning one of those values.
- An ADT can be parameterised by another type.
List
is parameterised: when given a typet
, it produces a type. EmptyList is an inhabitant of any List type.Cons
is what we call a type constructor. It can be applied like a function to a value of typet
and another value of typeList t
to produce a value of typeList t
. That may have been a bit confusing, so let's look at the examples. listA
is a value of typeList Number
. Remember, that is a single type. In this case,Cons
must be applied to aNumber
and anotherList Number
.listB
creates a similar structure, but withString
s instead ofNumber
s.- And
listC
is a member of any List type because EmptyList doesn't mention the type parametert
in its definition.
- Type aliases are a very simple feature.
- Maybe is aliased as Option. An Array of Numbers is aliased as NumberArray. List Number is aliased as NumberList. And a record with numeric
x
andy
fields is aliased as Point. - And just like ADTs, type aliases can be parameterised. Pair is shorthand for a record with left and right fields, and Predicate is an alias for any function that returns a Boolean.
- Pattern matching is an extremely powerful feature. In a parameter list, wherever we would use a variable, we can instead use a pattern. When a program has multiple consecutive definitions, failed pattern matches fail through to later definitions, kind of like switch-case in C-like languages.
- In the definition of map, we can see a pattern match on the value Nothing. In that case, the function returns a Nothing value. When the second parameter is not a Nothing, we know it must be a Just, and we can destructure the value out of the constructor.
map-prime
below is the same function, but implemented using pattern matching in acase
instead of the parameter list. Pattern matching in a parameter list is only sugar for pattern matching in a case expression in the function's body.
- Fields of a record can be pattern matched.
doubleZero
's first definition matches a record with anx
value of 0 and bindsy
to the record'sy
field andp
to the record itself using an at-pattern. If that pattern succeeds, it'll return a record with they
value doubled. Otherwise, execution will fall through to the second definition, which simply returns its argument. - And finally, the underscore in
const
allows us to ignore a parameter without binding it to a name.
- A typical PureScript program is split into one module per file.
- The module name is specified at the top, followed by imports, type declarations, and then value declarations.
- If you're like me, you're going to want to be a bit more explicit about what you import and export.
- Explicit exports may be listed after the module name, and explicit imports after the imported module name.
- Use the
qualified
keyword to import a whole module under a given name. - Modules that are intended to be executable will export a special function named main. This particular main function has no effects.
- Even though it's a bit verbose, this would be an idiomatic hello world program.
- What makes PureScript "pure" is that it tracks effects in the type system.
- The Eff constructor pairs a row of effects with a type. When an Eff is evaluated, a value of that type is produced, and the effects can be observed.
- For instance, the
random
value includes the RANDOM effect, which indicates that it affects the random number generator, and produces a Number. log
is a function that generates an Eff from a String. The resultant Eff includes the CONSOLE effect, which means that it will print to the console, and produces a Unit value. Unit values are used to indicate that the effects are the primary goal of the function. This function is similar to the console.log you already know, which is evaluated only for its effects and always returns null.print
is similar tolog
, but it's able to converts its argument to a String first.- This
main
function composes two effects using this operator that's pronouncedbind
. The important thing to know about this operator is that it passes the random number that was generated when evaluatingrandom
as the input to theprint
function. Notice that each individual effect is represented in the row of effects returned by main.
- When we want to compose additional effectful functions, it's convenient to use do notation. This makes a sequence of effectful function calls look more imperative.
- Remember that bind passes along the generated value to the following function. This
do
sugar allows us to save that value to a reference using the leftward facing arrow, which is pronounced "gets". In this case, we do that witha
andb
. - When the value is a Unit, we choose to just ignore it by omitting the arrow.
- Type classes are an unfortunately named feature, but they're incredibly useful for code re-use, enabling ad-hoc polymorphism. This means that functions can be defined in terms of the type classes that a type implements, much like how interfaces are used in programming languages that have interfaces.
- This is a type class in the standard library called Show. Any type
a
that implements Show must provide the definition for ashow
function that can generate a string for each of its values. This type class is what allowed theprint
function from earlier to convert its argument to a string. - Boolean implements Show very simply using a piece-wise definition: return the string "true" for true and the string "false" for false.
- Arrays can implement show as long as their parameter implements Show. The part of the type signature before the fat arrow is called a constraint. This one requires
a
to implement Show. This is necessary because in the recursive case we need to callshow
onx
, which is ana
.
- Finally, how can we interact with existing JavaScript?
- Use the
foreign
keyword to import in-scope names. You'll need to provide an appropriate type signature for each foreign value.parseFloat
is a function from String to Number. - Then, we define these values in a JavaScript file, attaching them to the exports object as we would in any CommonJS module.
- Remember, since PureScript curries its functions, we need to return a new function for each argument. So
pow
takes a base, then returns a function that takes the exponent and returns the result.
- We can also depend on foreign types and foreign effects. Here we're importing the values
setTimeout
andclearTimeout
along with the effects and types they need. Note that the names we choose for the foreign types and effects are arbitrary. setTimeout
takes an effectful function with a meaningless return value and a delay in milliseconds, and returns a value that represents both the effect that setTimeout has on the event loop and the unique identifier for this event.clearTimeout
takes one of those unique TimeoutID identifiers, and returns a value that represents the effect it has on the event loop.
- So at this point, you can't wait to start using PureScript right now.
- You can either grab a pre-compiled binary off Github or install it through one of these three package managers.
- To get a project started, I recommend using pulp, which is a project management tool designed specifically for PureScript.
- You can also use the Yeoman generator, which will create a Gulpfile along with the basic project structure.
- And of course you can just create the source tree and build files yourself.
- Building with pulp and gulp is easy.
- If you're doing it yourself, you'll need to use the psc and psc-bundle binaries directly.
- psc compiles all the PureScript and FFI files for your project and your dependencies.
- psc-bundle then combines the individual modules into a single executable JavaScript file.
- Dependencies are managed through bower. You can get bower through npm.
- Use the --save flag when installing runtime dependencies, and the --save-dev flag when installing development dependencies.
- To publish, you'll need to push up a git tag. You only have to register with bower the first time you publish. Publishing further versions is as easy as pushing a new git tag.
- Before you publish, you're going to want to make sure your library is well tested. We're going to write some property-base tests.
- It's a convention to define our tests in the
Test
namespace. We import our testing library, QuickCheck. We're going to test some invariants about the Data.Array.sort function. - The first invariant is idempotence: sorting an array twice should be the same as sorting it once.
- The second invariant is length preservation: sorting an array should not change its length.
- We export a main function that executes these tests in sequence.
- First we make sure we have the quickcheck dependency installed using bower install, then we can run our tests with pulp.
- QuickCheck will run 100 random inputs through each of our tests, and let us know if any of our invariants have been violated. These 100 random inputs could easily be a million by just telling QuickCheck how many tests you want.
- For those of you that prefer making individual assertions about your program's results, purescript-spec provides a familiar unit testing interface.
- Quickcheck tests can be run alongside the unit tests using a purescript-spec extension.
- The purescript-spec test runner also supports running tests in a browser environment.
- An easy way to get started playing around with PureScript, once you have it installed, is through PSCi, the PureScript REPL.
- Here, you can evaluate PureScript expressions, browse your loaded modules, and even ask the compiler to show you the types it infers for the expressions you enter.
- Pursuit is a search engine and package browser for PureScript libraries.
- I find it particularly useful for quickly looking up functions by their type signatures.
- The PureScript wiki has detailed information about each individual feature of the language. I recommend reading the FFI guide for some helpful FFI tips.
- If you need help with anything, the community is pretty active on the #purescript IRC channel on freenode.
- If you want to see everything put together into a real program, I recommend you look through the source of this very simple Mario game.
- It allows you to run and jump as Mario in a web browser, and it's written entirely in PureScript & HTML.
- It's intended to be both a beginner PureScript learning resource and an example of how to use PureScript in the browser.
- Once you've done that, I recommend you play around with the PureScript adaptor for the Express web framework.
- It comes with a decent example, and should allow you to actually build something useful at the same time.
- And we can even make desktop apps entirely in PureScript with nw.js through the purescript-node-webkit adapter.
- If you want to go in-depth with PureScript, and learn more about functional programming patterns in general, check out PureScript by Example on Leanpub.
- It was written by Phil Freeman, the original author of the PureScript compiler.
- And lastly, don't mistake my excitement for PureScript with an endorsement to go out and start running it on your pacemakers. It's still quite young and under heavy development.
- There are many big changes in these early pre-1.0 releases. Some features we're looking forward to in 0.7.1 and beyond: generic deriving, exhaustivity checker, improved error messages and warnings, JVM/C++11 backends, parallel builds, provenance for type variables in errors, ECMAScript 6 in FFI, and data kinds. One of the notable features PureScript is missing is constraint inference, so if you're writing polymorphic functions with constraints, you'll need to explicitly annotate them.
- I think PureScript is really cool, and it has tons of potential. JavaScript is everywhere, and we need a sane way to work with it. I believe its approach of using row types to represent JavaScript interfaces, while stealing all the good parts from Haskell, will lead to a very successful language.