Janet 1.38.0-73334f3 Documentation
(Other Versions:
1.37.1
1.36.0
1.35.0
1.34.0
1.31.0
1.29.1
1.28.0
1.27.0
1.26.0
1.25.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.17.1
1.16.1
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 provides 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 the 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."
[x]
(def oldx *my-binding*)
(set *my-binding* x)
(may-error)
(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
[x]
(with-dyns [:my-binding x]
(may-error)))
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."
[x]
(each name ['a 'b 'c 'd 'e 'f 'g]
(setdyn name @{:value (math/random)}))
nil)