DEV Community

Ben Clark
Ben Clark

Posted on

ReScript and Classless Coding

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
}

Enter fullscreen mode Exit fullscreen mode

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
  }

}

Enter fullscreen mode Exit fullscreen mode

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 

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Now when we run the program it prints:


Bob says Hello.
Bob is 29 years old

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)