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.