ReScript is one of my favorite languages right now. It's an easy-to-grasp functional-first language that transpiles to javascript. Unlike some other transpile-to-js languages, you don't need a jvm / .net runtime to get going. In fact it's faster than typescript to get going...
https://rescript-lang.org/docs/manual/latest/installation
It's syntax is essentially a subset of javascript with functional elements carefully crafted into it.
Not only is it easy to use, it makes code far far more predictable and safe.
ReScript however, doesn't have or need classes / prototypes.
In this post I want to show you how you can easily work in this classless paradigm.
Separate state from function
Firstly it helps to separate the concept of state from function.
In our example, we're going to create an instance of a person. This is done with a type
and a let
.
type person = {
name: string,
age: int
}
let bob = {
name: "Bob",
age: 28
}
In the above, since there is a type that matches bob's signature, bob's type is inferred to be person. We could have declared bob explicitly with let bob: person = { ... }
.
Now that we have our state, we can think about function(s)...
Group functions into modules
It's common to group functions that work on the same type of data into a common module. This is somewhat similar to methods within a class. In the below module we have a greet
and a tellAge
function.
Again, note that we haven't had to tell the functions that thisPerson
is of type person
because it is able to infer this.
module Person = {
let greet = thisPerson => {
thisPerson.name
->x => { x ++ " says Hello." }
->Js.log
thisPerson
}
let tellAge = (thisPerson) => {
open Belt.Int
thisPerson
->x => { x.name ++ " is " ++ x.age->toString ++ " years old" }
->Js.log
thisPerson
}
}
In ReScript you will often see the ->
operator which allows you to "pipe" the previous value into the following function. For example 10->increment->increment
is the same as increment(increment(10))
.
ReScript doesn't use the return
keyword, but rather returns the last expression in the function. Both of our functions return thisPerson
.
Putting it together
So now we "pipe" bob into one of the functions... let's say greet
.
// Note: In ReScript, a top-level expression must always be `unit`.
// `unit` is very similar to `undefined` is javascript.
// Since `bob->Person.greet` returns a type of `person` we use `ignore` to ignore this type and just return `unit` instead.
bob->Person.greet->ignore
Since Person.greet
returns back bob
, we can then continue to pipe into other Person
functions...
// Using open allows us to drop the need to write `Person.greet` and `Person.tellAge` and just use `greet` and `tellAge`
open Person
bob
->greet
->tellAge
->ignore
Note how we can use the ->
a bit like method chaining in OOP.
One cool thing with this style of syntax is that bob
doesn't have to be piped into a function from the Person
module and in fact can be piped into any function that accepts the signature.
For example, lets create a standalone function called incrementAge
...
let incrementAge = thisPerson => {
name: thisPerson.name,
age: thisPerson.age + 1
}
open Person
bob->incrementAge->greet->tellAge->ignore
Now when we run the program it prints:
Bob says Hello.
Bob is 29 years old
Immutable-first
You may have noticed that incrementAge
did not mutate bob
, but rather immutably produced a new version of bob
to continue passing through the pipe. This illustrates an important part of functional programming, in that where ever possible, the best approach is to use pure functions like this, that don't mutate existing values. We could then for example keep a current version of bob
and bob1YearFromNow
...
let bob1YearFromNow = bob->incrementAge
bob->greet->tellAge->ignore
bob1YearFromNow->greet->tellAge->ignore
Mutating Props
A good 90+ % of our code should be immutable, but what about when we just want to emulate a class and mutate some props! We can do that like so...
Firstly the person
type will need to explicitly call out that a particular property is mutable (Since everything is immutable by default). From there, we can create a function that accepts a person
and mutate the age
property. Again, we pass back thisPerson
, so that piping can continue should it need to.
// Updating person to have a mutable age
type person = {
name: string,
mutable age: int
}
let jill = {
name: "Jill",
age: 26
}
let mutIncrementAge = thisPerson => {
thisPerson.age = thisPerson.age + 1
thisPerson
}
jill->mutIncrementAge->ignore
Conclusion
So now we have seen how it's possible to emulate class-like behavior in ReScript, however when it comes to mutation - you will rarely see the mutation of single props like above. Mutation usually happens as immutably as possible in a functional-first language. That however sounds like a part 2.
Have you used ReScript?
How do you use a classless language?
Thanks all :)
Top comments (0)