3.1. symbol resolution

Both import and import* are actually shorthands and what they accomplish can be done using the lower-level builtins def, use and require. Here is how you could replace import:

#(with import:)
(import math)
(trace (math/+ 1 2))

#(with def and require:)
(def math (require "math"))
(trace (math/+ 1 2))

require returns a scope, which is defined as the symbol math. Then math/+ is resolved by looking for + in this nested scope. Note that the symbol that the scope is defined as and the name of the module that is loaded do not have to be the same, you could call the alias whatever you want:

#(this not possible with import!)
(def fancy-math (require "math"))
(trace (fancy-math/+ 1 2))

Most of the time the name of the module makes a handy prefix already, so import can be used to save a bit of typing and make the code look a bit cleaner. import*, on the other hand, defines every symbol from the imported module individually. It could be implemented with use like this:

(use (require "math"))
(trace (+ 1 2))

use copies all symbol definitions from the scope it is passed to the current scope.

Note that import, import*, def, and use all can take multiple arguments:

#(using the shorthands:)
(import* math logic)
(import midi osc)

#(using require, use and def:)
(use (require "math") (require "logic"))
(def midi (require "midi")
     osc  (require "osc"))

It is common to have an import and import* expression at the top of an alv program to load all of the modules that will be used later, but the modules don’t necessarily have to be loaded at the very beginning, as long as all symbols are defined before they are being used.

nested scopes

Once a symbol is defined, it cannot be changed or removed:

(def a 3)
(def a 4) #(error!)

It is, however, possible to ‘shadow’ a symbol by re-defining it in a nested scope: So far, all symbols we have defined - using def, import and import* - have been defined in the global scope, the scope that is active in the whole alv program. The do builtin can be used to create a new scope and evaluate some expressions in it:

(import string)

(def a 1
     b 2)

(trace (.. "first: " a " " b))
(do
  (def a 3)
  (trace (.. "second: " a " " b))
(trace (.. "third: " a " " b))
trace (.. "first: " a " " b): <Value str: first: 1 2>
trace (.. "second: " a " " b): <Value str: second: 3 2>
trace (.. "third: " a " " b): <Value str: third: 1 2>

As you can see, within a nested scope it is possible to overwrite a definition from the parent scope. Symbols that are not explicitly redefined in a nested scope keep their values, and changes in the nested scope do not impact the parent scope.