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:
Single Multiple Pull Function
Iterator
Push Promise
Observable
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 = () => unittype listener<'value> = (. 'value) => unittype listenerId<'value> = {id: int, listener: listener<'value>}
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 (likeconst
in JS/TS), but you can make them mutable by surrounding them withref()
, and then access the value withmyRef.contents
. Any React devs will find this nearly equivalent to theuseRef()
andmyRef.current
API that React provides. There is some syntax sugar that means thatmyRef.contents = 7
can be written asmyRef := 7
.The
open Js.Array2
line "opens that module" into my current scope so that I can useforEach
rather thanJs.Array2.forEach
. It's a bit likeimport com.initech.frontend.js.array.*;
in Java.Why are there
Js.Array
andJs.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
overJs.Array
,Js.String2
overJs.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.Array2let value = ref(initial)let nextId = ref(0)let listeners: array<listenerId<'value>> = []{get: () => value.contents,set: (v: 'value) => {value := v// notify all listenerslisteners->forEach(({listener}) => listener(. v))},listen: (listener: listener<'value>) => {let id = nextId.contentsnextId := id + 1listeners->push({id: id, listener: listener})->ignore// notify listener of value on listenlistener(. 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 filter
ing 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!