Go and Ale
Ale is designed to be hosted within a Go process. And while it’s not particularly difficult to embed or even extend Ale, getting data structures from Go into Ale (and vice versa) has historically been a high-friction activity. There’s a reason for this and it all comes down to equality.
Equality in Ale ≠ Equality in Go
Ale employs what’s called “structural equality.” This means that for any value, even if it’s not the same instance of a value, Ale will dive into its content to check whether or not it contains the same stuff. For example:
(eq { :name "Ale" :age 3 :colors [:red :green :blue] }
{ :age 3 :colors [:red :green :blue] :name "Ale" })
In this case, even though the two objects are completely separate instances and their properties were set in different orders, Ale has determined that they are structurally identical to one another, and so the call to eq
returns #t
.
In Go, equality checking is a behavior that is, let’s say, “dubious”. Certain types aren’t capable of being checked for equality, not even if a value of the type is being compared to itself. For example:
f := func() {}
fmt.Println(f == f)
This code won’t compile. You’ll get an error saying "func can only be compared to nil"
. If you try to cheat by casting f as an interface{}
, the code will compile, but will produce a runtime panic of "comparing uncomparable type func()"
.
All this may seem quite strange, but I’m sure the Go authors had a good reason for it. Maps and Slices also display this behavior. There are ways to work around the problem in Go, such as wrapping those types in a struct
. There are also restrictions that Go developers simply learn to “live with,” like not using such types as the keys in a map
.
Ale, by design, has no such restriction.
(define-lambda double (x) (* x 2))
(define func-map {
double "a lambda that doubles"})
(func-map double)
This code creates a lambda called double
and then uses it as the key in func-map
, wherein it maps to the string "a lambda that doubles"
. It then retrieves that string using the actual lambda.
These major differences between Go and Ale have always been one of the reasons why I hesitated on developing a dynamic bridge between the two languages. Until now.
Two Great Tastes That Taste Great Together
The Ale ffi
package is capable of bridging quite a few of Go’s types into something that can be passed around within an Ale environment. Wrapping a Go value and making it available in Ale is relatively straight-forward:
package main
import (
"github.com/kode4food/ale/pkg/core/bootstrap"
"github.com/kode4food/ale/pkg/eval"
"github.com/kode4food/ale/pkg/ffi"
)
type myStruct struct { // step 1
Greeting string `ale:"message"`
Farewell string
}
func main() {
s := &myStruct{ // step 2
Greeting: "Hello, World!",
Farewell: "Goodbye, Folks!",
}
env := bootstrap.TopLevelEnvironment()
bootstrap.Into(env)
ns := env.GetAnonymous()
w := ffi.MustWrap(s) // step 3
ns.Declare("my").Bind(w) // step 4
eval.String(ns, `(println (my :message))`) // step 5
eval.String(ns, `(println (my :Farewell))`)
}
In step 1 we define a Go type that we’ll be wrapping. In the case of a struct
, only Exported fields will be mapped by Ale. Ale will map field names to Keywords, but make no changes to their spelling or case. You may override this naming using the ale
field tag.
In step 2 we instantiate a copy of our struct, and store a pointer to it. Ale will recognize this type as a pointer to myStruct, and handle the indirection for you.
In step 3 we wrap the Go value. MustWrap
will panic if it cannot perform the wrapping. There is also a Wrap
function that will potentially return an error rather than panic.
In step 4 we bind the wrapped Go value into the Ale namespace under the name “my”. See embed for more information on how this process works.
In step 5 we ask the Ale evaluator to read, macro expand, compile, and execute our two calls to println
.
Type Mappings
The following table describes how a Go primitive type (referred to as a Kind
by the Go reflect package) will be mapped to a corresponding Ale type.
Go Kind | Ale Data Type |
---|---|
bool | Bool |
int(nn), uint(nn) | Number (Integer / BigInt) |
float(nn) | Number (Float) |
complex(nnn) | Cons<Float, Float> |
string | String |
array, slice | Vector |
struct | Object |
func | Function |
interface | Object (of bound Functions) |
map | Object |
chan | Channel Object |
The following Go kinds are not yet mapped | |
uintptr | unsafe.Pointer |