Making Reactive Datatypes in 200 lines of Nim

Sigil becomes more than a name

Sigils now supports first-class reactive data types! I wish I could say it was a grueling undertaking, but it wasn’t. Thanks to the core of Sigil’s being signals and slots along with Nim’s superb meta-programming it was pretty trivial to knock out. The trickiest part has been finding out what behaviors the JavaScript implementations follow.

Coincidently, Svelte’s reactive datatype is called Rune meaning ”a mark or letter of mysterious or magic significance” while Sigil means “an inscribed or painted symbol considered to have magical power“. Almost like this is what the Sigils library was meant to become.

I’m excited since this neatly solves a longstanding wart in Figuro UIs for how to provide a nice clean and simple API for handling reactive data. Signals and slots are powerful but a bit verbose and my initial APIs left me feeling underwhelmed. Reactive datatypes are much nicer:

    let x = newSigil(5)
    let y = newSigil(false)
    let z = computed[int]():
      if y{}: x{} * 2 else: 0

    check x{} == 5
    check y{} == false
    check z{} == 0
    y <- true #(* update the calue in y *)
    check y{} == true
    check z{} == 10

Previously in Figuro I was trying to do something similar but it just wasn’t it:

type Main* = ref object of Figuro
       counter = Property[int]()

proc draw*(self: Main) {.slot.} =
   withWidget(self):
      rectangle "count":
        text "btnText":
          bindProp(self.counter)
          node.setText({font: $(self.counter.value) & " ₿" }, Center, Middle)    
      Button.new "btnAdd":
        text "btnText":
          node.setText({largeFont: "–"}, Center, Middle)
        ## something like this:
        self.counter.onSignal(doClicked) do(counter: Property[int]):
          counter.update(counter.value-1)

This can now become more streamlined and provide a semantics familiar to webdevs (note I haven’t wired this bit into Figuro at publication time):

type Main* = ref object of Figuro
       counter = newSigil[int](0)

proc draw*(self: Main) {.slot.} =
   withWidget(self):
      rectangle "count":
        text "btnText":
          node.setText({font: $(self.counter{}) & " ₿" }, Center, Middle)    
      Button.new "btnAdd":
        text "btnText":
          node.setText({largeFont: "–"}, Center, Middle)
        node.onSignal(doClicked):
          self.counter <- self.counter{} - 1

Sigil[T] Villain’s Origin Story

The other day I posted on Nim-lang’s #appdev Discord channel about adding some animation support to Figuro. Using the new signals and slots from my new Sigils made creating a new more powerful Fader primitive super easy.

The Fader Agent (Sigil’s core object) fades in or out on a linear scale and can be interrupted midway and start going the other direction. In a couple dozen lines it seamlessly computes the next step based on Figuro’s doTick core signal when it begins fading and disconnects itself when it’s finished. Fader provides a core primitive needed for many key UI operations include fading a button on hover or sliding a menu in or out.

Phil, a fellow participant in #appdev (aka IsoFruit), asked about reactive datatypes. Phil does a lot of webdev and apparently *signals* have taken over the JavaScript UI world. Given that I had just made Fader which is a very reactive objects, I was curious what he meant and we started discussing it. He mentioned reactive data types were becoming standard in JavaScript with them being called Observables in RxJs, Signals in Angular, and Runes in Svelte.

After some back and forth it turned out he was referring to Reactive Programming. It’s an interesting field and I wasn’t sure how it’d fit into a static language like Nim. I’d experimented with it briefly way back in 2014 (almost a decade now :wow:) to run a PID loop on a Raspberry Pi. That experiment didn’t turn out well and was one of my first forays into latency sensitive programming and dealing with the pain of GC.

Nowadays Reactive Programming has moved into the mainstream and the method de jour of handling reactive user interfaces. That’s basically all UIs. Here’s a short example given by Phil:

const x = signal(5)
const double = computed(() => x()*2)
x.set(2)
// double is now 4

Well I figured that looks simple. I bet Figuro (really Sigils) could make short work of that. A short 20 minutes later I had a working example! Sorta, well behold:

    type
      Reactive = ref object of Agent
        value: int
    proc setValue(tp: Reactive, val: int) {.signal.}
    let
      x = Reactive(value: 5)
      y = Reactive()

    proc computed(self: Reactive, val: int) {.slot.} =
      self.value = val * 2
    x.connect(setValue, y, computed)

    emit x.setValue(2)
    echo "Y: ", y.value

This was lacking some syntactic sugar, but proved to me that it could be done. The next day I added a couple of template's and got:

  test "reactive wrapper":
    let
      x = reactive(5)
      y = computed(x): # probably need a macro to get rid of the extra x
        2 * x{}

    x <- 2
    echo "X: ", x.value
    echo "Y: ", y.value
    check y.value == 4
0
Subscribe to my newsletter

Read articles from Jaremy Creechley directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaremy Creechley
Jaremy Creechley