I gave this talk at JSFoo, Bengaluru, in October 2018. It describes my experience in switching to ReasonML on the Turaku project and how learning to use a statically-typed functional programming language involves much more than learning the language's syntax or features.
You can find the complete set of slides on Slideshare.
I'd like to thank Rekha for the last minute work that she did in replacing my awful stick figures with prettier ones, and Jasim and Sherin of Protoship, for their critique and advice in the process of creating this talk. Credit is also due to the fine folks over at HasGeek for setting up rehearsals; I'm pretty sure the talk wouldn't have gone as well as it did without those.
I've been working on a personal project for a little under a year now - a password management tool for teams. I started work on it because I looked at nearly every password manager out there and there was something or the other about all of them that ticked me off. Something that I thought could have been done better, or something that I just plain hated.
So I decided to build a new one. Why not? I've been building web applications for years now. How hard can a password manager be? So, I picked libraries and tooling that I was familiar with. I wanted an application that could run on the desktop, so I picked React & Electron to build a front-end, and Ruby on Rails for the API.
Turns out, it's not as easy as I thought it'd be.
After a few months of work, the front-end was giving me trouble. I noticed that adding new features, and refactoring was becoming a pain. Since I was building a proof-of-concept, I hadn't bothered with tests, and adding new features was becoming more and more stressful.
So I was faced with a choice. Either I start writing tests, and do TDD, or something akin to that going forward, or I try out something that I'd been hearing about for a while from what felt like all directions:
“Introduce static types!” It was something I had practically no experience with, but something I'd been hearing a lot about, and it seemed cool, so I thought, why not? I might as well learn something totally new while I'm at it.
So I shopped around, and I found four choices for me at the time: Typescript and Flow, if I wanted to keep my codebase intact, and gradually introduce typing, or switch to ReasonML, or Elm, and rewrite what I'd already created.
I ended up choosing ReasonML because of a couple of things:
How hard is it to get started? Not at all. Reason's tool-chain is also based around the Node ecosystem.
These two commands are all you need to run to get a minimum working Reason development environment.
First, and most obviously, ReasonML is statically typed. If a program compiles, then every binding has a type, even if we haven't specified anything. Because Reason has a type inference mechanism that is really intelligent.
From my experience with it, the best way to characterize the type system is that it feels like a person is sitting and trying to figure out what the types could be. It works really well, almost all the time.
When we look at the code here, we know that
What's different about records is that they need to have an explicit type definition.
You don't however need to specify that
myFirstCar is a vehicle. We can tell the type from looking at the code; so can Reason.
You'll also note that, there's no type information on the JS side. The type info is something that the compiler uses to make sure that there are no mistakes in what we've written. If the code compiles, then it's because it has a 100% certainty that every single one of the values and functions in our code uses the correct types.
Reason also has another feature that absolutely blew my mind the first time I used it - Variants. Variants allow us to model different possibilities in a way I never had in any dynamically typed language.
We can define
colour as a variant, that is either
Pink. In Reason, each of these values is called a constructor.
Let's use variants to try and improve the previous example.
Using variants for
The mind-blowing part of variants is when we pattern-match on them with
productionRun function, given a
car, returns the production run for the model of the car.
switch allows us to pattern-match things against their possible values. Note how there's no return statement. In Reason, the final expression mentioned in a function body is return value.
You'll also note that's there's some weird stuff in the JS output now.
That's because we're ignoring a warning from the compiler telling us thatwe've forgotten to handle two possible values here:
Zen. The compiler is aware there is a possibility that the function will fail, because the pattern matching in the function is not exhaustive. By default, it will issue a warning that the pattern-matching doesn't cover every possibility.
If we follow the compiler's instruction and write the function as it should have been written in the first place, you'll see that the JS output is much cleaner, with a simple switch-case.
Assuming that you use variants to describe different possibilities for values in your code, this removes a lot of over-head in terms of what you, as a developer, need to keep in mind when you're making changes. Because the compiler will remind you, if you make mistakes.
So because we have a compiler checking for possibilities that you might have forgotten to handle, these variant types are already useful. They're not perfect, though, because with these types, we can do something like this:
This compiles, but is very odd, because I've never seen a pink Ambassador on the road.
They exist, sure. There's nothing stopping you from painting an Ambassador pink, questionable as the choice may be. But pink was certainly never a production colour for that model.
While the previous example was within the realm of possibility, this one's a bit different.
That's just plain wrong. Maruti, the manufacturer, is never going to make an Ambassador. But it still compiles, because as far as the compiler's concerned, a
car is simply a record with these three separate values.
But they're not really separate are they? They're related to each other. Cars of a make could be one of a few models, and depending on the model, they may be available in a set of colours.
ReasonML variants have a feature that allows us to represent this sort of nested relationship. We can write this by giving variant constructors arguments, as shown here.
We have three sets of colours, one for each model, and there are two model variants, one for each manufacturer, and each of its constructor specifies that a model requires its corresponding colour as an argument.
And now that the properties have a nested relationship, we can simply say that a
car variant is one of two makes, each accepting a matching model variant.
In this example, variant constructors have only one argument, and they're also variants, but constructors can have any number of arguments, and they can be any type, so you put rich information in there, like records, or arrays.
Now, with this type structure, is is possible to create a pink Ambassador? Or a pink Maruti Ambassador? No, you can't.
You are only allowed to write correct combinations.
This process that I just demonstrated is a pattern that you'll find applied again and again in functional programming languages, and those with strong type systems, and it's called...
When types are written correctly, it can prevent you, the developer, from even writing an invalid data structure. A lot of our errors happen because our applications go into an invalid state. Types, used correctly, can make that impossible - because with a perfect type structure you literally cannot make mistakes.
So this approach takes care of data that we define inside ReasonML. Now we need to wonder whether this idea affects how we handle user input. You see, data could come from outside Reason in unexpected ways.
Assume that's there a UI which allows the user to select the properties of the car. Because the UI isn't well designed, it allows the selection of any combination of values. So if the user were to submit a form like this, we could presumably expect our application to end up with three random string values.
However, remember that functions in our code will use our custom type
car. If we want to pass this data around in our Reason code, we'll first want to fit this into our Reason data structure, with its strong type that we created earlier.
The whole point of creating that data type was to make sure illegal states were impossible. So how are we going to map this data to the vehicle type?
The short answer is that you can't. There is no conceivable way that invalid data is going to turn into a valid
car. So what can we do here? What are we supposed to do here?
So where am I going with this? Let's do a short recap. We've built a type
car with the explicit purpose of never being in an illegal state. We call this pattern make illegal states unrepresentable. We did this by using a ReasonML language feature calledvariants, and by relying on the ReasonML compiler to block us from creating invalid data.
But because we did this, we can't just naively fit user input into our data types. We need to parse them and handle unexpected data properly. What you'll really want to do when you get data from the outside world, and you will get invalid data from the outside world, is to properly parse that data and to handle every single edge case.
This is another pattern, and I'm calling this parse all external data & enforce boundaries.
I'm not going to go into detail into how this pattern can be implemented, but suffice to say that we can do it by using ReasonML's pattern-matching feature, which we very briefly saw in one of the examples, and by writing parser functions that use pattern-matching.
So we started with one pattern, and ended with two, and both patterns are enabled by language features that ReasonML provides. However, there are more patterns.
And this is certainly not exhaustive list - just a few things that I'm aware of. These patterns are enabled by the presence of language features - features that are only really available if you're using a strongly typed language.
When someone talks about learning a functional language like ReasonML, they don't mean getting used to the syntax, or even the language features. Those are relatively easy to learn. These patterns should be what you're looking to pick up.
I believe that building a repertoire of such patterns is what makes us better developers. If we can think of language features as the tools that we use to do our work, then we can also think of these patterns as the techniques that we learn to use the tools properly.
Understanding new techniques improves our ability, our craft, and the quality of the applications that we make.
What you really want to be focused on when you're learning a functional language are those patterns that I just mentioned, and I've personally found ReasonML to be a kind, forgiving companion in that journey.
Give it a try - I can guarantee that you'll learn something.