• Stars
    star
    134
  • Rank 270,967 (Top 6 %)
  • Language
    Go
  • License
    GNU General Publi...
  • Created almost 10 years ago
  • Updated over 5 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

goast is a Go AST utility with the aim of providing idiomatic meta-programming facilities for Go.

goast

goast is a Go AST (Abstract Syntax Tree) based meta-programming utility with the aim of providing safe, idiomatic code generation facilities by taking advantage of Go's native AST abilities and the go:generate build directive.

goast's core philosophies are

  • Compile time type safety
  • No runtime type casting (see previous)
  • Avoid runtime reflection
  • Prefer pure Go over syntax extensions
  • No text templates (see previous)
  • Prefer inference over annotation

The functionality of goast is currently built on the following axiom and proposition

  1. The empty interface (interface{}) can be replaced with any other type
  2. Any composite type composed at least partially of the empty interface (e.g. map[string]interface{}) can be replaced with any other composite type of the same structure with the empty interface swapped out for a concrete type (e.g. map[string][]int64)

Installing

Installing goast is as easy installing any other go package; Use the go get command to install from the canonical import path:

go get goast.net/x/goast

Usage

goast is designed as a general purpose command line utility for reading, examining, writing, and working with Go files and their AST. It follows a cli command subcommand pattern similar to git and go.

This document will focus primarily on a single subcommand goast write impl, but you can get more information on any other commands by typing goast help.

Please note: While functional, goast is still in an alpha/RFC phase so more subcommands may be added over time and subcommand structure may change (hopefully not)

Simple example

Consider the following generic implementation of a filtering method on a slice of empty interface

//file: slicefilter.go
package gen

type T interface{}
type Slice []T
func (s Slice) Where(fn func(T)bool) (result Slice) {
	for _, v := range s {
		if fn(v) {
			result = append(result, v)
		}
	}
	return
}

This code compiles, and accurately reflects the algorithm that any slice type might implement. There can be some initial confusion where a new gopher would expect the following to work

package main

import "fmt"

type Slice []interface{}

func (s Slice) Where(fn func(interface{}) bool) (result Slice) {
	for _, v := range s {
		if fn(v) {
			result = append(result, v)
		}
	}
	return
}

func main() {

	var s Slice = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	evens := s.Where(func(val interface{}) bool {
		i := val.(int)
		return i%2 == 0
	})

	fmt.Printf("Evens: %+v\n", evens)
}

Our intrepid gopher is a little sad about needing to use type casting, but is surprised to discover the following compile error main.go:18: cannot use []int literal (type []int) as type Slice in assignment (Playground). Surely a slice of ints provides the same interface as a slice of empty interface? The explanation for this is that an []int cannot be assigned to an []interface{} because they have different memory layouts. This is unfortunate because algorithmically the code is correct and it seems intuitive that it could be interpreted that way.

It is possible to rewrite the above by using var s Slice = []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9}, but converting back and forth between slice types on top of already needing to do type casting is onerous and a great place for bugs to creep in.

goast write impl exploits the fact that you can write compilable, generic Go code in that manner to provide typesafe code generation of a specific implementation of that meta-code.

With goast, a developer only needs to provide the following code, and the go generate command will provide the rest

//file main.go
package main

import "fmt"

//go:generate goast write impl slicefilter.go

type Ints []int

func main() {
	var s Ints = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	evens := s.Where(func(val int) bool { return val%2 == 0 })
	fmt.Printf("Evens: %+v\n", evens)
}

The go:generate build directive instructs goast to write an implementation of the code in slicefilter.go for the types provided in main.go, resulting in the following file being generated

//file ints_slicefilter.go
package main

func (s Ints) Where(fn func(int)bool) (result Ints) {
	for _, v := range s {
		if fn(v) {
			result = append(result, v)
		}
	}
	return
}

Playground

A reader following the code closely might have noticed that there is no longer a need for type casting within the callback function to Where. goast infers concrete type information from your source file and rewrites the the meta-code to be type-safe for your types, thus providing the ability to write correct, generic code once, and use it in an opt-in, type-safe manner.

A more complete slice iteration library can be seen in the iter package.

A Note About Paths: The examples above use relative paths to implment the code in a single, local file. goast also supports fetching code from the $GOPATH. The above example could be rewritten to use the iter package like so:

package main

import "fmt"

//go:generate goast write impl goast.net/x/iter

type Ints []int

func main() {
	var s Ints = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	evens := s.Where(func(val int) bool { return val%2 == 0 })
	fmt.Printf("Evens: %+v\n", evens)
}

Complex Example

Vanilla iteration patterns aren't exciting enough for you?

Want something more Go-centric, maybe even concurrency or parallel oriented?

How about a Fan-Out/Fan-In Concurrent Pipeline?

If you're not familiar with pipelines, this is a great primer on the pattern and pitfalls of implementing it. The concept is strightforward, but it's not the kind of thing I can see trusting myself to write repeatedly.

With goast you can have type-safe pipeline facilities for any type by defining a channel for that type and generating from the corresponding meta-code. A simple implementation of this pattern is provided in the pipeline package.

The following is a simple example of concurrent, parallel squaring of a range of numbers

package main

import (
	"fmt"
	"runtime"
)

//go:generate goast write impl goast.net/x/pipeline
type IntPipe chan int

func init() {
	runtime.GOMAXPROCS(runtime.NumCPU())
}

func main() {
	done := make(chan bool)
	defer close(done)
	workers := runtime.NumCPU()

	var ch IntPipe = make(chan int)
	go func(ch IntPipe) {
		for i := 0; i < 1000; i++ {
			ch <- i
		}
	}(ch)

	//Fan squaring out over a number of workers and collect the first 20 results
	result := ch.
		Fan(done, workers, func(n int) int { return n * n }).
		Collect(done, 20)

	done <- true
	for _, i := range result {
		fmt.Println(i)
	}
}

Related Types

goast also allows for the generation of derived composite types based on the provided type information, and calls these Related Types. Unlike other types seen in examples so far, the end-user does not need to provide an implementation of the Related Types, only the concrete types used to replace the generic segments of it. To accomlish this, goast also needs to ability to generate new, unique names for Related Types, and allows the meta-code developer to specify how these names should look using a convention of leaving a space for type name replacement with an '_' in the identifier of the Related type

The easy example of this would be a slice sorter Related Type

package sort // import "goast.net/x/sort"

import "sort"

type I interface{}
type Slice []I

//Related Type: Enables sorting as a slice operation
type _Sorter struct {
	Slice
	LessFunc func(I, I) bool
}

func (s _Sorter) Less(i, j int) bool {
	return s.LessFunc(s.Slice[i], s.Slice[j])
}

func (s Slice) Len() int {
	return len(s)
}

func (s Slice) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

//Sorts the slice in-place using the related type sorter
func (s Slice) Sort(less func(I, I) bool) {
	sort.Sort(_Sorter{s, less})
}

When that is compiled against a type such as type Contacts []*Contact, the Contacts type gets a Sort(func(*Contact, *Contact)bool) method, and a Related Type that looks like the following is generated

type ContactsSorter struct {
	Contacts
	LessFunc	func(*contact, *Contact) bool
}

This enables easy arbitrary sorts

//go:generate goast write impl goast.net/x/sort

type Contact struct {
	Id    int
	First string
	Last  string
	Email string
}

type Contacts []*Contact

func main() {

	set := getContacts()
	
	set.Sort(func(a, b *Contact)bool {
		return a.Id < b.Id
	})

	set.Sort(func(a, b *Contact)bool {
		return a.Last > b.Last
	})
}

Multiple Type Parameters

Any number of types are allowed to be specified in template code. For each desired type, assign a new identifier to interface{}.

The following defines a type that maps any type to a slice of any other type

type K interface{}
type V interface{}
type SliceMap map[K][]V

Partial Structs

goast write impl also allows for an amount of structural duck-typing with partially defined structs. This provides a type-safe ability to say "If it looks like X, it can do Y".

A toy example of this would a 'quittable struct'

//quittable.go
package main

type T struct {
	quit chan bool
}

func (t *T) Quit() {
	t.quit <- true
}

Implemented against

package main

//go:generate goast write impl quittable.go
type Process {
	data <- chan string
	value int
	quit chan bool

}

File Naming Control

It can be useful for organizational purposes for generated files to have a naming scheme that identifies them as a generated file. goast provides the --prefix and --suffix flags on the impl sub-command to control this behavior.

Example: The following produces a file named gen_ints_iter.go

package main

//goast write impl --prefix=gen_ goast.net/x/iter

type Ints []int

Roadmap

goast is still in an alpha/RFC stage of development. Some features that are planned for v1 are

History and acknowledgements

I originally got interested in code generation as a method of genericty in Go when I learned about the gen package from clipperhouse. When to Go team first announced Go 1.4 and the go:generate proposal, it planted the seed of the idea for goast in my brain and initiated my research into how it might work. In the intervening time I found gotgo, and more recently (and also quite close to my goals) the gonerics package. As projects in the same area as what goast explores, they were all valuble for research and inspiration, as well for providing a contrast against which I wanted to differentiate.

Licence

goast is released under a GPL v2 licence.