Janet 1.16.1-87f8fe1 Documentation
(Other Versions: 1.15.0 1.13.1 1.12.2 1.11.1 1.10.1 1.9.1 1.8.1 1.7.0 1.6.0 1.5.1 1.5.0 1.4.0 1.3.1 )

Dynamic Bindings

There are situations where the programmer would like to thread a parameter through multiple function calls, without passing that argument to every function explicitly. This can make code more concise, easier to read, and easier to extend. Dynamic bindings are a mechanism that provide this in a safe and easy to use way. This is in contrast to lexically-scoped bindings, which are usually superior to dynamically-scoped bindings in terms of clarity, composability, and performance. However, dynamic scoping can be used to great effect for implicit contexts, configuration, and testing. Janet supports dynamic scoping as of version 0.5.0 on a per-fiber basis — each fiber contains an environment table that can be queried for values. Using table prototypes, we can easily emulate dynamic scoping.

Setting a value

To set a dynamic binding, use the setdyn function.

# Sets a dynamic binding :my-var to 10 in the current fiber.
(setdyn :my-var 10)

Getting a value

To get a dynamically-scoped binding, use the dyn function.

(dyn :my-var) # returns nil
(setdyn :my-var 10)
(dyn :my-var) # returns 10

Creating a dynamic scope

Now that we can get and set dynamic bindings, we need to know how to create dynamic scopes themselves. To do this, we can create a new fiber and then use fiber/setenv to set the dynamic environment of the fiber. To inherit from the current environment, we set the prototype of the new environment table to the current environment table.

Below, we set the dynamic binding :pretty-format to configure the pretty print function pp.

# Body of our new fiber
(defn myblock
 (pp [1 2 3]))

# The current env
(def curr-env (fiber/getenv (fiber/current)))

# The dynamic bindings we want to use
(def my-env {:pretty-format "Inside myblock: %.20P"})

# Set up a new fiber
(def f (fiber/new myblock))
(fiber/setenv f (table/setproto my-env curr-env))

# Run the code
(pp [1 2 3]) # prints "[1 2 3]"
(resume f) # prints "Inside myblock: [1 2 3]"
(pp [1 2 3]) # prints "[1 2 3]"

This is verbose so the core library provides a macro, with-dyns, that makes it much clearer in the common case.

(pp [1 2 3]) # prints "[1 2 3]"
# prints "Inside with-dyns: [1 2 3]"
(with-dyns [:pretty-format "Inside with-dyns: %.20P"]
  (pp [1 2 3]))
(pp [1 2 3]) # prints "[1 2 3]"

When to use dynamic bindings

Dynamic bindings should be used when you want to pass around an implicit, global context, especially when you want to automatically reset the context if an error is raised. Since a dynamic binding is tied to the current fiber, when a fiber exits the context is automatically unset. This is much easier and often more efficient than manually trying to detect errors and unset context. Consider the following example code, written once with a global var and once with a dynamic binding.

Using a global var

(var *my-binding* 10)

(defn may-error
 "A function that may error."
 (if (> (math/random) *my-binding*) (error "uh oh")))

(defn do-with-value
 "Set *my-binding* to a value and run may-error."
 (def oldx *my-binding*)
 (set *my-binding* x)
 (set *my-binding* oldx))

This example is a bit verbose, but most importantly it fails to reset *my-binding* if an error is thrown. We could fix this with a try, but even that may have subtle bugs if the fiber yields but is never resumed. However, there is a better solution with dynamic bindings.

Using a dynamic binding

(defn may-error
 "A function that may error."
 (if (> (math/random) (dyn :my-binding)) (error "uh oh")))

(defn do-with-value
 (with-dyns [:my-binding x]

This looks much cleaner, thanks to a macro, but is also correct in handling errors and any other signal that a fiber may emit. In general, prefer dynamic bindings over global vars. Global vars are mainly useful for scripts or truly program-global configuration.

Advanced use cases

Dynamic bindings work by a table associated with each fiber, called the fiber environment (often "env" for short). This table can be accessed by all functions in the fiber, so it serves as place to store implicit context. During compilation, this table also contains top-level bindings available for use, and is what is returned from a (require ...) expression.

With this in mind, there is no requirement that the first argument to (setdyn name value) and (dyn name) be keywords. These functions can be used to quickly put values in and get values from the current environment. As such, these can be used for getting metadata for a given symbol.

(dyn 'pp) # -> prints all metadata in the current environment for pp.
(setdyn 'pp nil) # -> will not work, as 'pp is defined in the current environments prototype table.
(setdyn 'pp @{}) # -> will define 'pp as nil.
(def a 10)
(setdyn 'a nil) # -> will remove the binding to 'a in the current environment.

Macros can modify this table to do things that are otherwise not possible in a macro.

(defmacro make-defs
  "Add many defs at the top level, binding them to random numbers determined at compile time."
  (each name ['a 'b 'c 'd 'e 'f 'g]
    (setdyn name @{:value (math/random)}))