erikras.com
HomeAbout

Creating an Observable Subject in ReScript

Creating a Subject is a good way to test your skills in a new language.

Posted in Coding, ReScript
March 30, 2021 - 5 min read
Photo by Harrison Broadbent
Photo by Harrison Broadbent

The Observer pattern is very powerful, and those familiar with my work on Final Form will know that I'm a fan. The RxJs docs define Observables as:

Observables are lazy Push collections of multiple values. They fill the missing spot in the following table:

SingleMultiple
PullFunctionIterator
PushPromiseObservable

They can represent streams of data or a series of events over time. I like to use them to notify interested parties to changes of values (a.k.a. "state") over time. My favorite kind of observable is called a Subject. Back to the RxJs docs:

A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners.

I like to think of a Subject sort of like a physical object on a table. Vizualize a Rubik's Cube on the desk in front of you. You can look at it and see its position. If I go and move it, as soon as I do, if you're still looking at it, you will immediately know its new position. But if you look away, I can change it a lot and you won't know until you look back at it.

The Perfect Exercise

Creating a Subject is a nice exercise to try when learning a new programming language, as it requires:

  • Passing functions as arguments
  • Adding to a data structure of items
  • Iterating through a data structure of items
  • Removing items from a data structure

So what a great task to tackle in my online learning of ReScript!


Due to recent musings, I'm going to use the verb listen() rather than subscribe().


Types

The most important contract with observables is that the listen() function returns a function to "unlisten", to unregister the listener. We're going to define our unlisten type, our listener type, and a record type that keeps a unique id along with our listener. This is to tell them apart for unlistening.

type unlisten = () => unit
type listener<'value> = (. 'value) => unit
type listenerId<'value> = {id: int, listener: listener<'value>}
Remember that `unit` is like `void` in TypeScript

Now let's define the shape of our Subject...

Sometimes you can have a Subject and you want to know what its value is without subscribing to all future updates – a "glance" at the Rubik's Cube, if you will. One way to do this, following the strict Observer pattern, is to quickly listen and unlisten. In JavaScript, that would look like:

let value;
subject.listen((v) => (value = v))();
// ^^-- immediately unlisten

But that's gross, and the internal implementation always has the value anyway, so I like to provide a simple get() function on my Subjects.

type subject<'value> = {
listen: listener<'value> => unlisten,
get: unit => 'value,
set: 'value => unit,
}

Notice our Subject is strongly typed with a Type Parameter, also known as a "Generic" in other languages. All type parameters are prefixed with a ', which I learned the hard way, is not a backtick! unit is like void in TypeScript.

Okay, I think we're ready to attempt the...

Implementation

A few things to notice for non-ReScript folks:

  • let bindings, by default, are immutable (like const in JS/TS), but you can make them mutable by surrounding them with ref(), and then access the value with myRef.contents. Any React devs will find this nearly equivalent to the useRef() and myRef.current API that React provides. There is some syntax sugar that means that myRef.contents = 7 can be written as myRef := 7.

  • The open Js.Array2 line "opens that module" into my current scope so that I can use forEach rather than Js.Array2.forEach. It's a bit like import com.initech.frontend.js.array.*; in Java.

    • Why are there Js.Array and Js.Array2 modules, with identical docs? ReScript is still a growing language that hasn't quite figured out what their API dogma is yet. If that's a dealbreaker for you, then avoid it. Quoth the docs:

    • Js.Xxx2 Modules

      Prefer Js.Array2 over Js.Array, Js.String2 over Js.String, etc. The latters are old modules.

  • ->ignore is required because ReScript doesn't like you calling functions that return a value and you not doing anything with it, so you have to explicitly ignore the value.

export createSubject = (initial: 'value) => {
open Js.Array2
let value = ref(initial)
let nextId = ref(0)
let listeners: array<listenerId<'value>> = []
{
get: () => value.contents,
set: (v: 'value) => {
value := v
// notify all listeners
listeners->forEach(({listener}) => listener(. v))
},
listen: (listener: listener<'value>) => {
let id = nextId.contents
nextId := id + 1
listeners->push({id: id, listener: listener})->ignore
// notify listener of value on listen
listener(. value.contents)
() => {
let index =
listeners->reducei(
(foundIndex, listener, index) =>
listener.id === id ? index : foundIndex,
-1,
)
if index >= 0 {
listeners->removeFromInPlace(~pos=index)->ignore
}
}
},
}
}

When I implement this in TypeScript or JavaScript, I will often store the listeners in a Record<number, Listener> rather than an array, to allow for easier removal, but it doesn't look like ReScript supports that syntax. And I wasn't paying enough attention on Big O Day in Data Structures 201 to know which is better. ReScript also supports linked lists, which pleases someone who still remembers what CAR and CDR are, but they aren't useful for this random access deletion needed for my Subject.

I couldn't find a "remove the item for the first time this predicate is true and stop iterating" function, so rather than filtering through the array (and making the reference mutable), I chose to search for the index (which still goes through the entire array) and then delete (mutating) with that.

Thanks for reading! Comments and suggestions via Twitter are welcome!

Discuss on Twitter

© 2023 Erik Rasmussen