gen alpha
A library for bringing generics-like functionality to Go
gen is an attempt to bring some generics-like functionality to Go, with inspiration from C#’s Linq, JavaScript’s Array methods and the underscore library. Operations include filtering, grouping, sorting and more.
The pattern is to pass func’s as you would pass lambdas in Linq or functions in JavaScript.
Concepts
gen generates code for your types, at development time, using the command line. gen is not an import; the generated source becomes part of your project and takes no external dependencies.
The generated code implements the methods described below. To make this possible, a new slice type is generated for the struct types you specify. We call it the plural type. For example, if you gen an existing type Thing, a new plural type will be created:
type Things []Thing
…and you’ll use this wherever you would otherwise use a slice.
myThings := Things{...}
otherThings := myThings.Where(func).Sort(func)
Installation
Of course, start by installing Go, setting up paths, etc. Then:
go get github.com/clipperhouse/gen
To see usage, simply type:
gen
Create a new Go project, and cd into it. Create a main.go file and define a struct type in it. To quickly create the gen methods:
gen package.Thing
…using your package and type names. You should see a new file, named [your struct]_gen.go. Have a look around. Try out that plural type.
Usage
After installation, you should have access to a new gen command. cd into the directory containing the type(s) you wish to gen. Simply type gen to see usage.
package.TypeName
*package.TypeName
Generate plural type & methods for specified type name within the specified package. Prepend with optional * to operate on pointers (recommended) or omit it to use values.
-all
-*all
Generate plural type & methods for all struct types discovered within the current directory. Prepend with optional * to operate on pointers (recommended) or omit it to use values. Abbreviate -a.
-exported
Limit generation to exported struct types, i.e., those starting with capital letters. Useful with -all. Abbreviate -e.
-force
Force generation to continue even if errors. Abbreviate -f.
Methods
Signatures use the example type *Thing.
Where
Returns a new slice (plural type) whose elements return true for passed func. Comparable to Linq’s Where and JavaScript’s filter.
func (rcv Things) Where(fn func(*Thing) bool) Things
Example:
shiny := func(p *Product) bool {
return p.Manufacturer == "Apple"
}
wishlist := products.Where(shiny)
Count
Returns an int representing the number of elements which return true for passed func. Comparable to Linq’s Count.
func (rcv Things) Count(fn func(*Thing) bool) int
Example:
countDracula := monsters.Count(func(m *Monster) bool {
return m.HasFangs()
})
Any
Returns true if one or more elements returns true for passed func. Comparable to Linq’s Any or underscore’s some.
func (rcv Things) Any(fn func(*Thing) bool) bool
Example:
bueller := func(s *Student) bool {
return s.IsTruant
}
willBeHijinks := students.Any(bueller)
All
Returns true if every element returns true for passed func. Comparable to Linq’s All or underscore’s every.
func (rcv Things) All(fn func(*Thing) bool) bool
Example:
mustPass := func(t *Thing) bool {
return !t.IsEternal
}
cliché := things.All(mustPass)
First
Returns first element which returns true for passed func. Comparable to Linq’s First or underscore’s find.
func (rcv Things) First(fn func(*Thing) bool) (*Thing, error)
Example:
come := func(c *Customer) bool {
return c.IsHere
}
served, err := people.First(come)
Returns error if no elements satisfy the func.
Single
Returns unique element which returns true for passed func. Comparable to Linq’s Single.
func (rcv Things) Single(fn func(*Thing) bool) (*Thing, error)
Example:
id := request.Id
byId := func(t *Thing) bool {
return t.Id == id
}
item, err := things.Single(byId)
Returns error if multiple or no elements satisfy the func.
Each
Invokes passed func on every element. Comparable to underscore’s each.
func (rcv Things) Each(fn func(*Thing))
Example:
update := func(s *Score) {
s.Recalc()
}
scores.Each(update)
Sort
Returns a new slice (plural type) whose elements are sorted based on a func defining ‘less’. The less func takes two elements, and returns true if the first element is less than the second element.
func (rcv Things) Sort(less func(*Thing, *Thing) bool) Things
Example:
byRank := func(a, b *Player) bool {
return a.Rank < b.Rank
}
leaderboard := player.Sort(byRank)
SortDesc works similarly, returning the elements in reverse order. Its implementation negates ‘less’, so is effectively Sort(equalOrGreater).
IsSorted(Desc) uses a similar idiom, returning true if the elements are sorted according to the ‘less’ comparer.
Distinct
Returns a new slice (plural type) representing unique elements. Comparable to Linq’s Distinct or underscore’s uniq.
func (rcv Things) Distinct() Things
Example:
snowflakes := hipsters.Distinct()
Keep in mind that pointers and values have different notions of equality, and therefore distinctness.
DistinctBy
Returns a new slice (plural type) representing unique elements, where equality is defined by a passed func.
func (rcv Things) DistinctBy(func(*Thing, *Thing) bool) Things
Example:
hairstyle := func(a *Fashionista, b *Fashionista) bool {
a.Hairstyle == b.Hairstyle
}
trendsetters := fashionistas.DistinctBy(hairstyle)
Min
func (rcv Things) Min(less func(*Thing, *Thing) bool) (*Thing, error)
Returns the element containing the minimum value, when compared to other elements using a passed func defining ‘less’. Returns an error when invoked on an empty slice, considered an invalid operation.
Example:
byPrice := func(a, b *Product) bool {
return a.Price < b.Price
}
cheapest, err := products.Min(byPrice)
In the case of multiple items being equally minimal, the first such element is returned.
Max
func (rcv Things) Max(less func(*Thing, *Thing) bool) (*Thing, error)
Returns the element containing the maximum value, when compared to other elements using a passed func defining ‘less’. Returns an error when invoked on an empty slice, considered an invalid operation.
Example:
byArea := func(a, b *House) bool {
return a.Area() < b.Area()
}
roomiest, err := houses.Max(byArea)
Subsetting
By default, all of the above standard methods are created when you gen a type. If you would prefer only to generate specific methods, you can include a specially-formatted “tag” comment, which follows the convention of struct tags.
// gen:"Count,Where,Sort"
type Thing struct {
Name string
Year int
Sales float64
}
Give the comment its own line adjacent to the type defintion as you would with (or in addition to) godocs, and avoid spaces.
(Note: previous versions of gen put the comment within the type definition.)
Custom methods
Note: Custom methods are likely to be deprecated in favor of more-flexible “projection methods”. See this GitHub issue for details.
Custom methods use struct tags to project fields of a struct.
Given an example type:
type Thing struct {
Name string `gen:"Select,Aggregate"`
Year int `gen:"GroupBy,Select,Min"`
Sales float64 `gen:"Sum,Max,Average"`
}
You can infer the format from above. (Avoid spaces between methods.)
Below, {{Name}} and {{Type}} represent the name and type of a tagged field. The actual name will be substituted when you gen.
Select
func (rcv Things) Select{{Name}}() []string
Returns a slice of a specific field. Comparable to Linq’s Select or underscore’s pluck.
Example:
names := myThings.SelectName() // => ["Thing One", "Thing Two"]
GroupBy
func (rcv Things) GroupBy{{Name}}() map[{{Type}}]Things
Groups elements into a map keyed by the tagged field’s value. Comparable to Linq’s GroupBy or underscore’s groupBy.
Example:
report := myThings.GroupByYear() // => { 1995: ['Thing One', 'Thing Two'], 2007: ['Thing Three', 'Thing Four'] }
Sum
func (rcv Things) Sum{{Name}}() {{Type}}
Sums a field over all elements of Things. Comparable to Linq’s Sum.
Example:
revenue := myThings.SumSales() // => 68885.74
Average
func (rcv Things) Average{{Name}}() {{Type}}
Sums a field over all elements and divides by len(Things). Comparable to Linq’s Average.
Example:
avg := myThings.AverageSales() // => 30005.74
Max
func (rcv Things) Max{{Name}}() {{Type}}
Selects the largest value of a field in Things. Comparable to Linq’s Max.
Example:
bigmoney := myThings.MaxSales() // => 685698.99
Min
func (rcv Things) Min{{Name}}() {{Type}}
Selects the least value of a field in Things. Comparable to Linq’s Min.
Example:
earliest := myThings.MinYear() // => 1995
Aggregate
func (rcv Things) Aggregate{{Name}}(fn func({{Type}}, {{Type}}) {{Type}}) {{Type}}
Iterates over Things, operating on each element while maintaining ‘state’. Comparable to Linq’s Aggregate or underscore’s reduce.
Example:
var join = func(state string, value string) string {
if state != "" {
state += ", "
}
return state + value
}
list := myThings.AggregateName(join) // => "Title One, Title Two, etc"
FAQ
First off: we’re alpha! Caveat emptor. Feedback welcome.
Why?
Go doesn't (yet) offer generic types, and we are accustomed to many of their use cases. Perhaps you are similarly accustomed, or would find them useful.
Code generation, really?
Yes. We do it to ensure compile-time safety, and to minimize magic. It’s just code, and you can read it.
Codegen is not without its risks, we understand. But keep in mind that languages which do support generics are in fact doing something like code generation, so perhaps gen’s approach is not that far out.
gen creates a new slice type for your selected struct(s) because method receivers cannot be slices. It’s clearer and less verbose to type myThings.Where(fn) than package.Where(myThings, fn). Not to mention, the latter wouldn’t work for multiple struct types in the same package.
You re-implemented sort?
Yes. Go’s sort package requires the fulfillment of three interface members, two of which are usually boilerplate. If you want to sort by different criteria, you need to implement multiple ‘alias’ types.
gen’s Sort requires a single argument defining ‘less’, and no interface implementation. You can call ad-hoc sorts simply by passing a different func.
gen’s implementation is a strongly-typed port of Go’s implementation. Performance characteristics should be similar.
Could some of this be achieved using reflection or interfaces + type assertions?
Perhaps! It’s early days and the focus is on the API and compile-time safety. We’ve no doubt that more idiomatic approaches will reveal themselves.
What about imported types?
gen doesn’t support imported types that might be members of your struct, reason being that we’d have to resolve those dependencies and include them in the generated code. This isn’t impossible, we may get there.
As a workaround, you might create a local ‘alias’ type, something like:
type MyType otherPackage.Type
// or...
type MyType struct {
otherPackage.Type
}
Why didn’t you implement X method?
Most likely:
- We haven’t gotten to it yet, or
- We haven’t found an idiomatic way to do it, or
- It doesn’t save you any code, or
- It duplicates something that Go does well.
Here are some design criteria.
Can I use it?
We’d be thrilled if you would test it out and offer feedback. It’s still very early alpha bits, caveat emptor for production use. The API will likely be volatile prior to 1.0.
Can I help?
Sure, the code is here.
Who is ‘we’?
Matt Sherman, mostly. You can reach him @clipperhouse on GitHub or Twitter. Matt Jibson is helping out as well.