Intro to PureScript for TypeScript developers
Teaser for the benefits of strongly typed pure functional programming
PureScript is a statically typed general-purpose programming language inspired by Haskell, compiled into JavaScript. The vision of the language is to make frontend development more productive by leveraging an expressive type system and primarily focusing on supporting functional programming techniques.
To see why such a language might be interesting, first let's take a short detour to see what might be missing from TypeScript, one of the most popular languages for the frontend nowadays.
The Limitations of TypeScript's Type System
TypeScript's type system is powerful, but there are certain things that it does not care about.
Consider the following function:
function divide(a: number, b: number): number {
...
}
Can you guess the implementation?
Well, it could be a / b
, but it could be also this:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
const result = a / b;
console.log(`${a} divided by ${b} is ${result}`);
globalState.mystuff = 'workwork';
http.post('https://my-example.com', { data: 'dummy data' }).catch((error) => {
console.error('API error:', error.message);
});
return result;
}
Unarguably, this method does a bit more than described in the signature:
- The function logs to the console, sends an HTTP request, and changes the state of an object as a side effect.
- The caller is forced to handle the potentially thrown exception explicitly.
For divide
only the return type is guaranteed by the type system, but it is completely blind about these characteristics. However, they are very important.
If the function throws an exception, the call site has to prepare for that because it can change the control flow. If it performs an HTTP request or changes the global state,
it might impose limitations on where and when it can be used.
Furthermore, since the type system is unaware of these factors, they can easily go unnoticed. While they are easily identifiable in this simple example, they may be present in any function calls executed by this function.
With TypeScript one can encode all this information through types, but the language does not impose any strict enforcement on it.
Enter PureScript
PureScript approaches this question a bit differently. The goal of the language is to encourage users to use purely functional constructs almost everywhere instead of allowing a mix of multiple paradigms and to support it with an expressive and strict type-checking mechanism.
PureScript's "strict" and "purely functional" nature makes it more picky when it comes to compiling code, but it also offers some unique features to support writing code in functional style.
First functions
In PureScript everything is an expression. There's no special syntax for function declaration. They can be created anywhere and can be returned or passed as arguments:
welcomeMessage greeting name = greeting <> " " <> name <> "!"
welcomeMessage
takes two arguments and can be used as follows:
welcomeMessage "Hi" "John"
-- It returns the String "Hi John!"
PureScript has a powerful type-inference mechanism, so it's not required to explicitly add the type annotations, but for clarity it's recommended to do so for top level functions. With additional types, the definition looks like the following:
welcomeMessage :: String -> String -> String
welcomeMessage greeting name = greeting <> " " <> name <> "!"
This leads to the first interesting language feature: all functions have just one parameter.
If you pass only a single argument to this function, it will return a function that takes a single string:
welcomeMessageHu :: String -> String
welcomeMessageHu = welcomeMessage "Szia"
welcomeMessageHu "John"
"Szia John!"
When welcomeMessage
is called with two arguments, it's equivalent to calling the function one argument, then calling the resulting function with the second.
With this one can create new functions without additional efforts from existing ones with some arguments already injected.
This is also possible in other languages, but typically it requires some work to transform a function into a form to work like this.
Everything is in the types
While focusing primarily on pure functions may require additional effort initially, it ultimately alleviates concerns regarding hidden exceptions and side effects in the long term.
divide :: Int -> Int -> Maybe Int
divide x y =
if y == 0 then Nothing
else Just (x / y)
The Maybe
type can be considered very similar to Optional in Java. In case of a zero divisor the function returns Nothing
, the "empty" version of Maybe
. Otherwise it just wraps the
result of the calculation. This way, there's no need to use a different language construct (a try-catch) to handle the unusual case because everything is encoded in the returned value.
Unwrapping the Maybe
type to the value can be done explicitly on the caller side by checking imperatively if Maybe
is Nothing
, but there are better approaches.
Most wrapper types like Maybe
support map
to perform operations on their contents without explicitly dealing with the Nothing
case. This is very similar how map is used in JavaScript to transform arrays.
With this it's possible to define a chain of operations without explicitly dealing with the Nothing
case:
map (\n -> n + 1) (map (\n -> n * 3) (divide 6 2))
-- Just 10
If divide
returns Nothing
, no operations will be performed. Otherwise, the map
calls will operate on the wrapped value.
It's also possible to combine multiple functions that return a wrapper with concatMap
or bind
(in TypeScript it's called flatMap
). PureScript even provides a nice syntactic sugar
for it in the form of the do notation to avoid deeply nested code:
calculation :: Maybe Int
calculation = do
a <- divide 6 2
b <- divide 9 0
c <- divide 8 4
pure (a + b + c)
The symbol <-
can be read as "selects" while pure
is to transform the resulting value back to the Maybe
type.
In case one of the divide
calls returns Nothing
the result of the whole function will also be Nothing
and calling further function calls will be skipped.
It can be made even more interesting with the Either
type which is like Maybe
but also holds a potential error message.
Handling side effects
If Maybe
is a wrapper for a value that may or may not exist, then Effect
is a similar wrapper for a value that may produce side-effects when it is computed.
divide :: Number -> Number -> Effect Number
divide a b = do
let result = a / b
log $ show a <> " divided by " <> show b <> " is " <> show result
pure result
Effect
in the signature makes it obvious that the function has a side effect, but it also prevents the function from being called from pure functions.
With this PureScript enforces the separation of actions (e.g. side-effecting code) and pure calculations.
Effect
can be generally used for side-effecting code, not just logging, like changing mutable state, sending an HTTP request, manipulating the DOM, or even throwing an exception.
Summary
To wrap it up, the key features of PureScript are enforcing purely functional programming and having a strong type system. Both aspects can be considered as incentives to utilize the language but also as deterrents from using it.
Due to these two characteristics requires the user to encode most things as types, giving a compile-time feedback about correctness instead of finding surprises much later at runtime. Providing earlier error detection is a plus, but it also means more up-front work is required to get to working code.
Being purely functional means it's very different from most popular languages. Because of this PureScript might also be a good learning tool to get immersed in functional programming. While it's possible to use functional programming techniques in TypeScript, it is a multi-paradigm language, allowing the users to write object-oriented and procedural code; as a user it requires discipline to use functional programming to the fullest and not to revert to old habits of object-oriented or procedural programming at the first obstacles.
On the flip side, learning opportunity also means learning requirement when it comes to onboard new people to PureScript code, making it hard to scale teams.
In this post I did my best to completely avoid functional programming jargon in order to focus on PureScript's important characteristics, but one needs to learn about category theory very soon in order to work effectively with the language. Analogies like "it's just a wrapper" or "it's almost the same as Optional from Java" are not sufficient and can lead to misunderstandings or incorrect implementations. This probably is one of the reasons why PureScript (and it's spiritual ancestor, Haskell) are used in the industry, they remain niche languages.
I believe knowing about practical functional programming can make one a better engineer. If I made you interested, I recommend the following sources to deep dive into this topic:
- PureScript by Example is a great book to learn about the language. You might also enjoy Learn You a Haskell for Great Good! since PureScript is heavily inspired by Haskell.
- Functional Design Patterns - Scott Wlaschin is a very good language-agnostic intro to idioms used in functional programming, covering monoids, monads, functors, currying and more. I also recommend a slightly different take from the same author: Railway oriented programming.
- Programming Languages course on Coursera by Dan Grossman is a great intro to the differences between different programming paradigms using Racket (lisp) ML (similar to PureScript) and Ruby (object-oriented).