Dynamic Parameters

Dynamic parameters are fiber-local variables with scoped rebinding. They provide dynamic scope without breaking lexical scope rules.

Creating parameters

(def *verbose* (make-parameter false))
(*verbose*)                # => false
(parameter? *verbose*)     # => true

Scoped rebinding

parameterize temporarily overrides a parameter's value for the dynamic extent of its body.

(def *indent* (make-parameter 0))

(defn show-level []
  (println (string/repeat "  " (*indent*)) "level " (*indent*)))

(show-level)               # "level 0"

(parameterize ((*indent* 1))
  (show-level)             # "  level 1"
  (parameterize ((*indent* 2))
    (show-level)))         # "    level 2"

(show-level)               # "level 0" (restored)

Built-in parameters

stdout and stderr are dynamic parameters. Rebinding them redirects output:

# (parameterize ((*stdout* my-port))
#   (println "goes to my-port, not terminal"))

Fiber inheritance

Parameters are fiber-local: each fiber has its own param_frames stack. A fresh fiber created by fiber/new automatically inherits a snapshot of the creating fiber's current parameter bindings — the snapshot is taken at creation time, not at resume time, so the child sees what the parent had bound the moment fiber/new was called.

(def *colour* (parameter :default))

(def f (parameterize ((*colour* :red))
         (fiber/new (fn () (*colour*)) 1)))

# parameterize has unwound here — `(*colour*)` would read :default —
# but the fiber captured :red at creation:
(fiber/resume f nil)
(fiber/value f)              # => :red

This matters for ev/spawn: the spawning fiber may finish (and its parameterize blocks may unwind) long before the scheduler resumes the spawned fiber. Without creation-time snapshotting the child would observe whatever bindings the scheduler happens to hold at resume time — typically nothing useful.

The snapshot is a copy. Reparameterizing in the creator after spawning does not affect already-created children, and the resumer's bindings do not override the child's snapshot:

(def *x* (parameter :default))

(def f (parameterize ((*x* :captured))
         (fiber/new (fn () (*x*)) 1)))

(parameterize ((*x* :resumer-has-this))
  (fiber/resume f nil))
(fiber/value f)              # => :captured

Inheritance is transitive — a fiber that spawns another fiber passes its snapshot down. There is no opt-in or opt-out: if you want fresh bindings inside the fiber, wrap its body in parameterize.


See also