Embedding Ale
Ale is designed to be hosted within a Go process, but with a catch – each environment you create is isolated from the others, meaning namespace modifications are not shared. So what could have been as simple as calling eval.String("(+ 1 2 3)")
suddenly becomes a few lines of code more complicated.
I should explain why this is.
Namespaces and Their Environments
Namespaces are where you put your stuff. They manage bindings from names to values. Bindings are immutable after being defined, though a binding can be declared before being defined, which I’ll explain later.
There are two kinds of namespaces: qualified and anonymous. A qualified namespace maps to a specific domain (example: “ale” or “user”). Anonymous namespaces map to no domain and garbage collection loves them.
Ale defines one qualified namespace by default. It is the root namespace, and is named ale. All of the built-in functions are registered there, and other namespaces will fall back to it when resolving unqualified names (for example: if
and defn
).
Environments establish isolation. They are responsible for instantiating namespaces on demand. When a domain-qualified namespace is first requested, an instance is produced, and the environment will return a reference to it thereafter. A new instance is produced every time an anonymous namespace is requested, and the environment will not retain a reference to it.
What an Ale Wants
In order to fully bootstrap an environment, Ale expects three bindings in its root namespace. These are *in*
, *out*
, and *err*
. You might have already guessed that these are somehow related to the standard input, output, and error files on most UNIX-like systems.
These names are not initially bound. Instead they’re only declared, but declaring them is enough to allow Ale to compile code that performs a runtime lookup. If they don’t eventually become bound, Ale will explode when you run code that requires them. So we have to get them bound somehow, right? Fortunately, Ale provides flexibility here. There are two pre-defined bootstrap environments (TopLevel and DevNull), and you can also create your own.
Let’s Bootstrap!
The following code will print “hello, world!” to standard output.
package main
import (
"github.com/kode4food/ale/pkg/core/bootstrap"
"github.com/kode4food/ale/pkg/eval"
)
func main() {
env := bootstrap.TopLevelEnvironment() // step 1
ns := env.GetAnonymous() // step 2
eval.String(ns, `(println "hello, world!")`) // step 3
}
In step 1 we call bootstrap.TopLevelEnvironment()
. This returns a newly instantiated top-level environment. This call performs a bootstrap.Into
that populates the environment’s root namespace with all of Ale’s built-ins. If it didn’t do this then we couldn’t even make a call to (def).
The top-level environment pre-binds *in*
, *out*
, and *err*
to the system’s standard file descriptors. That’s why you can see the result of the call to (println)
.
In step 2 we request a new anonymous namespace from the environment. Our call in step 3 will use this to bind any potentially introduced names.
In step 3 we ask the Ale evaluator to read, macro expand, compile, and execute the string "(println "hello, world!")"
.
After that, your copy of the environment and everything you did in the anonymous namespace are garbage collected.
See? Not So Tough!
Yeah, yeah, with other Lisps you can probably pull this off with a one-liner, but are three lines really so bad? Isolation in the host process is a very good thing.
If it’s really a problem for you, might I suggest this function:
import "github.com/kode4food/ale/pkg/data"
func OneLiner(src data.String) data.Value {
env := bootstrap.TopLevelEnvironment()
ns := env.GetAnonymous()
return eval.String(ns, src)
}
And then you can have your one-liner back.