Janet 1.42.0-dev-7fd75f3 Documentation
(Other Versions: 1.42.0 1.41.2 1.41.1 1.40.1 1.40.0 1.39.1 1.38.0 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 )

Bindings (def and var)

Values can be bound to symbols for later use using the keyword def. Using undefined symbols will raise an error.

(def a 100)
(def b (+ 1 a))
(def c (+ b b))
(def d (- c 100))

Bindings created with def have lexical scoping. Additionally, bindings created with def are immutable; they cannot be changed after definition. For mutable bindings, like variables in other programming languages, use the var keyword. The assignment special form set can then be used to update a var.

(def a 100)
(var myvar 1)
(print myvar)
(set myvar 10)
(print myvar)

In the global scope, you can use the :private option on a def or var to prevent it from being exported to code that imports your current module. You can also add documentation to a function by passing a string to the def or var command.

(def mydef :private "This will have private scope. My doc here." 123)
(var myvar "docstring here" 321)

Scopes

Defs and vars (i.e. bindings) live inside what is called a scope. A scope is simply where the bindings are valid. If a binding is referenced outside of its scope, the compiler will throw an error. Scopes are useful for organizing your bindings and can provide flexibility in expressing your programs. There are two main ways to create a scope in Janet.

The first is to use the do special form. do executes a series of statements in a scope and evaluates to the last statement. Bindings created inside the form do not escape outside of its scope.

(def a :outera)

(do
  (def a 1)
  (def b 2)
  (+ a b)) # -> 3

a # -> :outera

# compile error: unknown symbol b
b

Any attempt to reference the bindings created within the do form after it has finished executing will fail.

Note that in the example above, defining a inside the do form did not overwrite the original definition of a for the global scope. As the code in the example demonstrates, the original definition of a was still accessible after the do form. We say that a or its binding was "shadowed" within the do form.

Another way to create a scope is to create a closure. The fn special form does this by introducing a scope just like the do special form.

(def a :first!)
a # -> :first!

(def f
  (fn []
    # a new scope has been introduced
    (def b a)
    # shadowing a's outer binding
    (def a :inner!)
    # after the following, the new scope "ends"
    @[b a]))

# binding created in f's scope are not valid here
a # -> :first!
# compile error: unknown symbol b
b

# f can access those bindings though
(f) # -> @[:first! :inner!]

# shadow a's original binding
(def a :second!)
a # -> :second!

# f's bindings unaffected by immediately previous shadowing
(f) # -> @[:first! :inner!]

It also possible to create new scopes using the if and while special forms. In both cases, bindings can be introduced in their respective "condition" portions and the bindings will be valid within the rest of each of the forms.

# using def in if's condition
(defn f [x]
  (if (def n x)
    n
    (when (not n) :was-nil!)))

(f :something) # -> :something

(f nil) # -> :was-nil!

# using def in while's condition
(def arr @[2 1 0])
(while (def n (length arr))
  (array/pop arr)
  (when (zero? n) (break)))

arr # -> @[]

There is also a built-in macro, let, that does multiple defs at once, and then introduces a scope. let is a wrapper around a combination of a do and defs, and is the most "functional" way of creating bindings.

(let [a 1
      b 2]
  (+ a b)) # -> 3

The above is equivalent to the earlier example using do and def.

Note that the special forms def and var do not introduce new scopes and the following sort of code is valid.

(def a (def x 1))

a # -> 1
x # -> 1

(var b (var y 2))

b # -> 2
y # -> 2