Read time 12 min
In PureScript you get types, immutability, and functional programming “for free”– the trade-offs aren’t as steep. This is largely true of a few other languages as well, but let’s see what our discussion of fear and trust from the previous post looks like in PureScript from the same two perspectives: understanding the shape of data and changing data.
Fear and the shape of data
PureScript starts with a high level of trust in the data by default. What is the shape of our data? Whatever the type says it is.
loadUser function takes in a user ID and returns an asynchronous effect– think of Aff like a Promise– containing either the validated user or errors. We then take this response in
loadAndRenderUser and either render the user’s name, if we received a user, or log the errors if not.
User type, you know it has exactly what the type says: an ID, optionally an email address, and a name. Everything is also immutable unless the type tells you otherwise, so you know this data won’t change underneath you. There are no null values. There are no
any types. The value is what it says on the label. The compiler will tell you if you’re using it wrong. You can simply trust the data has the shape you expect, and will stay that way, and stop worrying about it.
What about data you get off the wire? Validation in this example happens with the call to
readJSON. This takes in something with an unknown shape – a
String – and attempts to turn it into a known shape – the
User. It returns either the parsed user data or errors that occurred in validating the data. This is the default way of handling data that comes from outside, and this means that after this point you can trust that the data has the shape of a
User. You’re not trusting the external service to provide correct data, because you validate the data as it comes in. You’re also not trusting developers to write correct types – if they write incorrect types, the validation will fail, and you will notice that.
Here’s the boilerplate for validating the data:
In most cases you don’t actually need to write any type validation logic. The compiler and library write it for you. If you have special needs, you can also write out this logic in a straightforward way manually.
The point is, as a developer you can simply trust that the data is shaped how the type says it is. What does this data look like? Does this field exist? Will changing this break the code? Read the type. Change it and let the compiler or a validation error tell you if it breaks something.
Fear and changing data
- It can’t mutate the data.
- It can’t modify your file system.
- It can’t launch rockets.
They’re not in the type. The type of the function is
This says you take in a
SourceDoc and a
String and return a
Document. It can’t return anything else. It can’t return null. It can’t do other side effects like changing the data or launching rockets. This is a stupidly simple way of thinking about functions and types. A function takes in something and returns something and does nothing else.
Suppose you actually do want to mutate the data. You could write it like this:
The type of our
formatDocument function changed:
Now the function takes in a mutable reference to a document, and a string, and returns something – an effect. The effect has a type
Eff _ Unit, which means that when this effect is run, it will do some side effects and return an empty value (
SourceDoc), but only that data – we still can’t change any other data in our program. We also can only change the data in ways that respect the developer’s trust. For instance, we can’t give the data a new field that is not in the type, because that would mean the type is wrong.
Suppose you actually want to launch a rocket from this function. You might write it like this:
Now our function type is
Because launching a rocket is a side effect, the function needs to return an effect. The launching of rockets is explicit in the type – you can’t launch rockets without writing functions that return effects. Users of those effects have to recognize that the function returns an effect and handle it accordingly. This still doesn’t tell you what kind of effect will be performed when this is run – it could be doing many types of side effects. But it tells you clearly that the function does other things than just return a document. And the effect hasn’t actually happened yet. You might decide later that you don’t want to do the effect, and never execute it.
You probably don’t need to do this. You probably don’t want to. But you could. The code starts with the assumption of types you can trust, pure functions, and immutability, and you can selectively weaken those assumptions as needed. Or you can make them stronger, by making your types more restrictive. You have the tools to do both.
Types supporting functional programming
There’s no need to choose between types, immutability, and functional transformations. In PureScript the ideas all support each other and are pervasive in the code and ecosystem. Here are two examples.
Types and plugging functions together
Or the same thing using the proposed `pipe` operator:
But as these pipelines get larger and the number and complexity of transformations grows it becomes harder and harder to trust that the transformations are working correctly. In theory optional typing could help with this, and you could just write the above and have TypeScript or Flow make sure the piping lines up. In simple cases the type inference for this will probably work fine and you can do that. Other times it seems to work, but it quietly wiped out your types in the middle of the pipe. Or the type inference doesn’t work at all, and you end up writing something like this, with the code logic drowned out in type annotations:
This is still a relatively simple example, but in this and especially larger examples you might also want to clean this up in a functional style. You might rewrite it like this:
In PureScript, you just write this:
More broadly, in PureScript the types help you make the piping line up for any types of transformations – data transformations, but also asynchronous network calls, or server middleware, or error handling, or config validations. When you change your code, the type system tells you whether what you wrote even makes sense with everything else that you wrote. Compared to TypeScript or Flow, PureScript is both more expressive and has better type inference. Together these mean you can write code like in a dynamic language, but keep the types. You can also use the types more easily in ways that are difficult or impossible in TypeScript or Flow, like when using function composition or lenses.
Types and immutability
Trust and the ecosystem
Learning to code without fear
In PureScript, there’s no need to choose between these ideas. The ideas all support each other and are pervasive in the code and ecosystem.
What do pervasive strong types, immutability, and functional programming give you? A high base level of trust in the code that you write. A feeling of relative security. The confidence to refactor code freely as needed.
Programming without the fear.