You need a reactive or stream library. I have tried a quick keywords search on nimbler directory, but cannot hit any.
I have an unpublished and unpolished library for reactive programming. Here is the base file.
import deques, tables
type
Callback[X] = proc(x:X): bool
Port*[X] = ref object of RootObj
publish*: Callback[X]
subscribe*: proc(cb: Callback[X])
InPort*[X] = ref object of Port[X]
## The only port with public `pub`. This can be considered as input.
EventPort*[X] = ref object of Port[X]
ValuePort*[X] = ref object of Port[X]
val: X
proc sub*[X](p: Port[X], cb: Callback[X]) {.inline.} =
## common use
p.subscribe(cb)
template subIt*[X](port: Port[X], body: untyped) =
port.sub proc(x : X): bool =
let it {.inject.} = x
`body`
proc pub*[X](p: InPort[X], x:X) {.inline.} =
## common use
discard p.publish(x)
proc get*[X](p: ValuePort[X]): X {.inline.} = p.val
proc mget*[X](p: ValuePort[X]): var X {.inline} = p.val
proc put*[X](p: ValuePort[X], x:X) {.inline.} =
p.val = x
discard p.publish(x)
proc newPort*(X: typedesc): Port[X] =
var cbs: seq[Callback[X]]
result.new()
result.publish = proc(x:X): bool =
var i = 0
var j = 0
let n = cbs.len
while j < n:
let cb = cbs[j]
let unsub = cb(x)
if unsub:
cbs[j] = nil
j.inc
else:
if unlikely(i != j):
cbs[i] = cbs[j]
cbs[j] = nil
i.inc
j.inc
cbs.setLen(i)
result.subscribe = proc(cb: Callback[X]) =
cbs.add cb
proc newInPort*(X: typedesc): InPort[X] =
let p = newPort(X)
result.new()
result.publish = p.publish
result.subscribe = p.subscribe
proc newEventPort*(X: typedesc): EventPort[X] =
let p = newPort(X)
result.new()
result.publish = p.publish
result.subscribe = p.subscribe
proc newValuePort*[X](x0:X): ValuePort[X] =
let p = newPort(X)
result.new()
let self = result
result.val = x0
result.publish = proc(x:X): bool =
self.val = x
p.publish(x)
result.subscribe = p.subscribe
proc cont*[X,Y](port: Port[X], cc: proc(x:X, cc: proc(y:Y))): EventPort[Y] =
## Continuation
result = newEventPort(Y)
let self = result
port.subscribe proc(x: X): bool =
cc(x, proc(y:Y) = discard self.publish(y))
proc map*[X,Y](port: Port[X], f: proc(x:X):Y): EventPort[Y] =
result = newEventPort(Y)
let self = result
port.subscribe proc(x:X): bool =
self.publish(f(x))
proc pipe*[X](a: Port[X], b: InPort[X]) =
a.subscribe(b.publish)
proc mem*[X](port: Port[X], x0: X): ValuePort[X] =
## Create a new ValuePort and `sub` to `port`
result = newValuePort(x0)
port.subscribe(result.publish)
proc once*[X](port: Port[X], cb: proc(x: X)) =
port.subscribe proc(x: X): bool =
cb(x)
result = true
template mapIt*[X](port: Port[X], body: untyped): untyped =
type Y = typeof(
block:
var it {.inject.}: X;
body
)
map[X, Y](
port,
proc(x:X): Y =
let it {.inject.} = x
`body`
)
If you variable is called x, read the value with x.get() and update the value with put(). Listen the value on change with subIt. Derive new reactive value with mapIt or yieldIt or cont. Depending on your requirement, you may need more advanced scheduler e.g. derived value update only when there is at least one subscriber. Also, unsubscribing by returning true is not general, it may not suite your needs.
An example
when isMainModule:
let x = newValuePort[int](0)
let y = x.mapIt(2*it) # derive y = 2*x
x.subIt:
echo "x=", it
y.subIt:
echo "y=", it
x.put(1)
x.put(2)
output
y=2
x=1
y=4
x=2
y=6
x=3
This isn't nearly as sophisticated, but you can do something like this with a regular getter/setter too. It won't work for a plain variable, but if it makes sense to tie it to an object, you can do this:
# robots.nim
type
Robot* = object
name: string
proc `name=`*(self: var Robot, name: string) =
self.name = name
echo "Robot name has been changed"
proc name*(self: Robot): string = self.name
# main.nim
import robots
var robot = Robot()
robot.name = "Dave"
echo "Our robot is named ", robot.name