Ko

A language for programming recursive circuits

Ko
3. Compiler
3.2. Gates: Transformations in Go
3.2. Gates: Transformations in Go

Gates are a mechanism for implementing transformations in Go and making them accessible within (call-able from) Ko.

Implementing a transformation in Go (a gate) requires three simple steps. First, describe the argument structure of the transformation by defining a Go struct for it. Second, implement the transformation as a Play method attached to the Go argument structure (from step 1). Third, register the implementation with the Ko compiler (in Go) and build an “extended” compiler which links in the new implementation.

In the following complete example, we imlpement a gate which returns the maximum of two 64-bit integers and is visible from Ko as the two-argument function GoMaxInt64(x, y) .

The entire implementation fits in a single .go file which we can place, say, in github.com/kocircuit/kocircuit/utils/integer/max.go .

package integer

import (
	"github.com/kocircuit/kocircuit/lang/go/eval"
	"github.com/kocircuit/kocircuit/lang/go/runtime"
)

func init() {   // Register the new gate with Ko.
	eval.RegisterEvalGate(new(GoMaxInt64))
}

type GoMaxInt64 struct {   // gate argument structure
	X int64 `ko:"name=x"`   // The Ko field name is specified in a tag.
	Y int64 `ko:"name=y"`   // There are no restrictions on field types.
}

// Play implements the gate transformation.
// Play can return any Go type as necessary.
func (g *GoMaxInt64) Play(ctx *runtime.Context) int64 {
	if g.X < g.Y {
		return g.Y
	} else {
		return g.X
	}
}

After Go package integer is linked into the compiler (described below), the transformation GoMaxInt64 will be accessible from Ko with the appropriate package import clause. For instance, the following Ko code utilizes GoMaxInt64 to return the maximum of three 64-bit integers:

import "github.com/kocircuit/kocircuit/utils/integer" as util

Max3(x, y, z) {
	return: util.GoMaxInt64(
		x: util.GoMaxInt64(x: x, y: y)
		y: util.GoMaxInt64(x: y, y: z)
	)
}

Monadic arguments

It is also possible to implement Go transformations, where one of the arguments is monadic (i.e. the transformation can be invoked with a shorthand notation by passing a single argument value and not specifying the argument name).

The following variation of GoMaxInt64 accepts a sequence of integers (the monadic argument int64s ) and an additional optional argument otherwise , determining the result if the sequence is empty:

type GoMaxInt64 struct {
	Int64s []int64 `ko:"name=int64s,monadic"`
	Otherwise *int64 `ko:"name=otherwise"`
}

func (g *GoMaxInt64) Play(ctx *runtime.Context) int64 {
	var max int64
	if g.Otherwise != nil {
		max = *g.Otherwise
	}
	for _, n := range g.Int64s {
		if n > max {
			max = n
		}
	}
	return max
}

This new implementation of GoMaxInt64 can be invoked (in Ko) in two different ways. The standard way entails passing the arguments and specifying their names:

util.GoMaxInt64(int64s: (1, 2, 3), otherwise: 0) // or
util.GoMaxInt64(int64s: (1, 2, 3))

The latter case, where the optional otherwise argument is not passed, can also be written as util.GoMaxInt64(1, 2, 3) , thereby taking advantage of the fact that int64s is defined as the monadic argument (default argument when no argument name is specified).

Type translations

The Go types used for the fields of the gate structure, as well as for the return value of the Play method, are generally unconstrained.

The Ko compiler transparently converts Go types to their unambiguously corresponding Ko types . Primitive types (boolean, string, signed and unsigned integers and floating-point numbers) are mapped respectively. Go structures and slices are mapped to Ko structures and sequences. Go pointers correspond to optional types in Ko. Go interfaces are mapped to Ko opaque values. (This facilitates transporting Go runtime objects, which might be mutable, through the Ko immutable type system without complications.) Go channels, arrays and functional types are — for the moment — disallowed as they are not in line (or necessary for) common protocol type systems (like Protocol Buffers or GraphQL). If their use is necessary, in cases of low-level systems programming with Ko, they can always be hidden behind a Go interface to be treated as opaque inside Ko.

Extending the compiler

Before the Ko compiler can see and reflect on new gates, they need to be linked into the compiler. We have provided an easy way to do so.

Create a new Go binary package (which will build to the new compiler) and import the Ko compiler infrastructure package, as well as any number of Go packages containing gate implementations. For the example from this article, you would simply need:

package main

import (
	"github.com/kocircuit/kocircuit/lang/ko/cmd"   // provides the Ko compiler command-line
	_ "github.com/kocircuit/kocircuit/util/integer"   // links the gate into the new compiler
)

func main() {
	cmd.Execute()
}

There are also simlpe methods (excluded here for brevity) to embed the Ko compiler in your software (rather than using it on the command-line) and utilize it , for example, as a “scripting engine”.