Comparing programming languages XIII: Retaking this series with ReScript

Re-introduction
In this series, I explore programming languages by implementing an interpreter for the Monkey Language.
An interpreter is a medium-sized project that allows me to explore most language features without getting into the particular library ecosystem. (The only library that the interpreter needs is a unit-testing one.)
Previously
In the last episode, we tried to speed up my interpreter written in Python
ReMonkey
ReMonkey is my implementation of the Monkey Language written in ReScript.
My history with ReScript
I have zero experience with ReScript. I had a brief look at its previous iteration, ReasonML, and read a couple of books about it but never wrote any serious projects with it.
What I like about ReScript
A soft landing on pure functional programming
ReScript is my first pure functional language (Not a Hybrid OOP/Functional language like Scala), and I love it. Previously, when I wrote Julieta in Lua, I used an emulation library to have OOP in it, but I don’t need it with ReScript. Apart from a few growing pains, everything was smooth sailing, and I didn’t miss OOP at all.
It helps a lot that ReScript isn’t crazy strict about immutable values. You must be explicit about it, but it is possible without too much friction.
Type system
The type system is so simple yet so powerful. I didn’t miss any of the crazy types from Scala/TypeScript. And when you have a simple type system, it is easy to reason about it and is easy to write tools around it, which leads to my next point…
Tooling
The ReScript LSP is very good. This is my first time writing a full project using NeoVim (Using the LazyVim distro, I don’t want or need to roll my own config) instead of a JetBrains IDE. Do not worry, the unofficial plugin for ReScript/OCaml/ReasonML is great, but I feel it wasn’t as powerful as the LSP.
The compiler is very fast, and the JavaScript that it produces is so charming. I never pay too much attention to the code that TSC generates, but the code that ReScript generates is, as described by the official website, “human readable“. So, for every function that I write, I always go and check what fascinating code the ReScript compiler has just generated.
What I don’t like about ReScript
No early returns
Give. Me. My. Early. Returns. Back. Now.
By far my biggest complaint. I wrote so much plumbing/clutter to get the same logic that an early return would give me. The hypocrisy is that the JavaScript that it generates is full of early returns… So those are good enough for you, but not for me…. I see.
Let me show with an example:
The entry function of my evaluator is named eval, and it receives a program (which contains a collection of statements) and an environment to store values.
It looks like this in Kotlin:
And it looks like this in ReScript:
I need an extra mutable value to stop processing the loop, and I mean “processing”; the iterations are still happening.
You can also see some code to manage the Option values that isn’t an exclusive problem with ReScript. It's just that in my honest opinion, Nullable types are way better than Option monads.
Lack of resources
Apart from one book (ReasonML had some books back in the day), and the forum, there is not a lot of content. The documentation is excellent, but finding an example for something slightly unusual is hard. (I also mourn the quality lost on our current search engines. No, I don’t want to see your GenAI slop, thank you.)
Naming “conventions”
So, lower case is for Types, the First uppercase character is for Type variants, and all uppercase is for Type variants as well. These are not conventions; the compiler actually enforces them.
I ended up with situations where a parameter name was the same as the type, and I know that I can use type inference, but it was so messy. I ended up using a lot of single-letter parameters and snake_case names, and I just don’t enjoy this. Probably skill issues on my part.
Let’s talk about performance
When we talk about performance for a language that compiles to JavaScript, we are talking about two things: The runtime (Bun, Node or others) and the “quality” (quality defined as how easy it is for a runtime to execute and optimise) of the code generated by the compiler.
For these tests, I’m going to compare ReScript with TypeScript and KotlinJS, using Bun, Node and Deno.
Language | Bun (s) | Node (s) | Deno (s) |
ReScript | 35.962 | 18.156 | 17.336 |
TypeScript | 16.851 | 11.249 | N/A |
KotlinJS | 32.266 | 31.865 | 46.144 |
To my surprise, Node is faster with every language, which was unthinkable a couple of years ago. Seems that the Node team is working seriously (or maybe Bun had some performance regression). Good Job.
Deno is a mixed bag, faster with ReScript, not running at all with my TypeScript project and slower with KotlinJS.
So the ReScript performance isn’t bad, and I can theorise that it isn’t as fast as TypeScript due to the usage of Option types. For sure there is room for improvement.
Update # 1
Hyeseong Kim created a PR fixing the performance problems with my code. Basically, I was using the functions from the ReScript Core library, like Option.flatMap
and Array.forEach
but the final API design isn’t finished yet (as v11). The API design should be complete by version v12, and then the optimisation works can start. Kim’s fix replaces the API calls with native ReScript switch
and loops.
The numbers look very good, with increases between 30 to 40% and getting very close to TypeScript.
Conclusion
I like ReScript. Isn’t perfect but is charming on its own quirkiness. If you’re overwhelmed by TypeScript’s complexity, give ReScript a chance.
Subscribe to my newsletter
Read articles from Mario Arias directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
