Skip to content

Derived signals

Mapping

Signal.map derives from one signal, map2 and map3 combine several:

open Fable.Ripple

let count = Var.create 2

let double = Signal.map (fun n -> n * 2) count
let label = Signal.map2 (fun n d -> $"%d{n} doubled is %d{d}") count double

printfn "%s" label.Value // 2 doubled is 4
count.Value <- 21
printfn "%s" label.Value // 21 doubled is 42

Auto-tracking

Signal.computed takes a plain function. Whatever it reads while running is a dependency - there is no list to declare:

open Fable.Ripple

let price = Var.create 10.0
let quantity = Var.create 2

let total = Signal.computed (fun () -> price.Value * float quantity.Value)

printfn "%.2f" total.Value // 20.00
quantity.Value <- 5
printfn "%.2f" total.Value // 50.00

Tracking under an if

An if decides what gets tracked. b is read only while readB is true, so it is a dependency only then:

let a = Var.create 1
let b = Var.create 10
let readB = Var.create false

let total =
    Signal.computed (fun () ->
        if readB.Value then
            a.Value + b.Value
        else
            a.Value
    )

Bump b with the switch off and nothing runs - there is no edge to carry the change:

Cached and lazy

A derived signal computes when read, and remembers the result until a dependency changes:

open Fable.Ripple

let count = Var.create 1

let double =
    Signal.computed (fun () ->
        printfn "computing..."
        count.Value * 2
    )

printfn "%d" double.Value // computing... then 2
printfn "%d" double.Value // 2 - served from the cache
count.Value <- 5 // marks the signal stale, nothing runs
printfn "%d" double.Value // computing... then 10

computing... prints twice, not four times: the second read is served from cache, and the write only marks the signal stale - the recompute happens at the next read.

One node, many readers

The cache is shared. A derived signal with several consumers runs once per change, however many read it - where a plain function would run once per caller:

open Fable.Ripple

let a = Var.create 1

let b =
    Signal.map
        (fun n ->
            printfn "b ran"
            n * 2
        )
        a

let left = Signal.map (fun n -> n + 1) b
let right = Signal.map (fun n -> n - 1) b

// Runs immediately: b ran, left = 3, right = 1
Signal.effect (fun () -> printfn "left = %d, right = %d" left.Value right.Value)
|> ignore

a.Value <- 2 // b ran (once), left = 5, right = 3

b ran prints once per write, not once per consumer.

Dynamic dependencies

Signal.bind chooses the dependency from a value. It is not a second mechanism: Signal.bind f s is Signal.computed (fun () -> (f s.Value).Value) - what bind adds is the flattening:

open Fable.Ripple

let useCelsius = Var.create true
let celsius = Var.create 21.0
let fahrenheit = Var.create 70.0

let shown =
    useCelsius
    |> Signal.bind (fun c ->
        if c then
            celsius.Signal
        else
            fahrenheit.Signal
    )

// Runs immediately: shown = 21.0
Signal.effect (fun () -> printfn "shown = %.1f" shown.Value) |> ignore

fahrenheit.Value <- 80.0 // not a dependency - nothing runs
useCelsius.Value <- false // shown = 80.0
fahrenheit.Value <- 90.0 // now tracked: shown = 90.0

While useCelsius is true, writing fahrenheit reruns nothing - it is not a dependency until the switch flips.

The rewiring, drawn

The same switch, as a picture: the arrow into Signal.bind follows whichever source it currently reads.

Bump the unwired source and nothing moves - no flash, no dot, no run. Flip the selector and the arrow jumps.

The signal

A computation expression over the same combinators. let! ... and! is applicative, a lone let! is monadic:

open Fable.Ripple

let first = Var.create "Ada"
let last = Var.create "Lovelace"

let full =
    signal {
        let! f = first
        and! l = last
        return $"%s{f} %s{l}"
    }

printfn "%s" full.Value // Ada Lovelace
last.Value <- "Hopper"
printfn "%s" full.Value // Ada Hopper

Each keyword compiles to one call:

let! a = x  and! b = y     //  Signal.map2 (fun a b -> ...) x y
let! a = x                 //  Signal.bind (fun a -> ...) x
return v                   //  Signal.constant v
return! s                  //  s, handed back untouched
Edit this page