├── templates ├── d │ ├── chan.go │ ├── pointer.go │ ├── slice.go │ ├── set.go │ └── graph.go └── f │ ├── functional2.go │ └── functional.go ├── README.md ├── script └── deploy ├── nginx-config ├── cgi.sh ├── template_params.go ├── ast_rewriter.go ├── template_params_test.go └── gonerics.go /templates/d/chan.go: -------------------------------------------------------------------------------- 1 | type Chan chan T 2 | -------------------------------------------------------------------------------- /templates/d/pointer.go: -------------------------------------------------------------------------------- 1 | type Pointer *T 2 | -------------------------------------------------------------------------------- /templates/d/slice.go: -------------------------------------------------------------------------------- 1 | type Slice []T 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gonerics 2 | 3 | This is the code that powers gonerics.io. More information at [http://bouk.co/blog/idiomatic-generics-in-go/](http://bouk.co/blog/idiomatic-generics-in-go/). 4 | -------------------------------------------------------------------------------- /templates/d/set.go: -------------------------------------------------------------------------------- 1 | type Set map[T]bool 2 | 3 | func New() Set { 4 | return make(Set) 5 | } 6 | 7 | func (set Set) Add(key T) { 8 | set[key] = true 9 | } 10 | 11 | func (set Set) Contains(key T) bool { 12 | return set[key] 13 | } 14 | 15 | func (set Set) Remove(key T) { 16 | delete(set, key) 17 | } 18 | -------------------------------------------------------------------------------- /script/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | SERVER=gonerics.io 5 | 6 | remote () { 7 | ssh $SERVER "$1" 8 | } 9 | 10 | remote "mkdir -p ~/gonerics" 11 | scp -r cgi.sh *.go nginx-config templates/ $SERVER:~/gonerics/ 12 | remote "cd ~/gonerics && export GOPATH=\"\$(mktemp -d)\" && go get -d && go build" 13 | remote "sudo ln -sf /home/bouke/gonerics/nginx-config /etc/nginx/sites-enabled/gonerics; sudo service nginx reload" 14 | remote "sudo rm -rf ~/git/*" 15 | -------------------------------------------------------------------------------- /templates/f/functional2.go: -------------------------------------------------------------------------------- 1 | type Function func(T) U 2 | type Function2 func(U, T) U 3 | 4 | func Map(f Function, input []T) []U { 5 | result := make([]U, len(input)) 6 | 7 | for i, element := range input { 8 | result[i] = f(element) 9 | } 10 | 11 | return result 12 | } 13 | 14 | func Reduce(f Function2, initial U, input []T) U { 15 | for _, v := range input { 16 | initial = f(initial, v) 17 | } 18 | return initial 19 | } 20 | -------------------------------------------------------------------------------- /nginx-config: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server ipv6only=on; 4 | 5 | server_name localhost; 6 | 7 | location ~ (?/(?g|d|f)/(?[a-zA-Z_0-9]+)/(?(?:[A-Za-z0-9_\-/]\.?)+))(?/(?:info/refs|git\-upload\-pack)) { 8 | gzip off; 9 | 10 | include fastcgi_params; 11 | fastcgi_param SCRIPT_FILENAME /home/bouke/gonerics/cgi.sh; 12 | fastcgi_param GIT_HTTP_EXPORT_ALL ""; 13 | fastcgi_param GIT_PROJECT_ROOT /home/bouke/git; 14 | fastcgi_param GIT_REPO_PATH $git_repo; 15 | fastcgi_param PATH_INFO $git_repo$path; 16 | 17 | fastcgi_param TEMPLATE_TYPE $template_type; 18 | fastcgi_param NAME $name; 19 | fastcgi_param PARAMETERS $parameters; 20 | 21 | fastcgi_pass unix:/var/run/fcgiwrap.socket; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /cgi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | GITDIR="/home/bouke/git$GIT_REPO_PATH" 5 | DIR=`pwd` 6 | 7 | if [ ! -d $GITDIR ]; then 8 | TDIR=`mktemp -d` 9 | cd $TDIR >/dev/null 2>/dev/null 10 | git init --bare >/dev/null 2>/dev/null 11 | 12 | TDIR2=`mktemp -d` 13 | cd $TDIR2 >/dev/null 2>/dev/null 14 | git clone $TDIR . >/dev/null 2>/dev/null 15 | /home/bouke/gonerics/gonerics --template-type="$TEMPLATE_TYPE" --name="$NAME" --parameters="$PARAMETERS" > file.go 2>>/home/bouke/git/gonerics.log 16 | git add file.go >/dev/null 2>/dev/null 17 | git commit -m "This repo is $NAME with params $PARAMETERS" >/dev/null 2>/dev/null 18 | git push origin master >/dev/null 2>/dev/null 19 | rm -rf $TDIR2 >/dev/null 2>/dev/null 20 | 21 | mkdir -p `dirname $GITDIR` >/dev/null 2>/dev/null 22 | mv -T $TDIR $GITDIR >/dev/null 2>/dev/null 23 | fi 24 | 25 | cd $DIR 26 | /usr/lib/git-core/git-http-backend 27 | -------------------------------------------------------------------------------- /templates/d/graph.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "gonerics.io/d/set/T.git" 3 | ) 4 | 5 | type Graph map[T]set.Set 6 | 7 | func New() Graph { 8 | return make(Graph) 9 | } 10 | 11 | func (g Graph) Connect(a, b T) { 12 | _, ok := g[a] 13 | 14 | if !ok { 15 | g[a] = set.New() 16 | } 17 | 18 | g[a].Add(b) 19 | } 20 | 21 | func (g Graph) Neighbours(a T) (ret []T) { 22 | if g[a] != nil { 23 | for key := range g[a] { 24 | ret = append(ret, key) 25 | } 26 | } 27 | 28 | return 29 | } 30 | 31 | func (g Graph) connected(a, b T, visited set.Set) bool { 32 | if visited.Contains(a) { 33 | return false 34 | } else { 35 | visited.Add(a) 36 | } 37 | 38 | for _, neighbour := range g.Neighbours(a) { 39 | if neighbour == b || g.connected(neighbour, b, visited) { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | func (g Graph) Connected(a, b T) bool { 47 | visited := set.New() 48 | return g.connected(a, b, visited) 49 | } 50 | -------------------------------------------------------------------------------- /template_params.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Import struct { 9 | Alias, Path string 10 | } 11 | 12 | type Params struct { 13 | Package string 14 | 15 | Imports []Import 16 | Types []string 17 | RawTypes []string 18 | } 19 | 20 | func Parse(params string) *Params { 21 | result := &Params{} 22 | 23 | result.RawTypes = strings.Split(params, PARAMETER_DIVIDER) 24 | result.Types = make([]string, len(result.RawTypes)) 25 | 26 | for i, v := range result.RawTypes { 27 | splits := strings.Split(v, ".") 28 | typeName := splits[len(splits)-1] 29 | 30 | pack := strings.Join(splits[:len(splits)-1], ".") 31 | 32 | if pack == "" { 33 | result.Types[i] = typeName 34 | } else { 35 | depName := fmt.Sprintf("dep%d", i) 36 | 37 | trimmedTypeName := strings.TrimLeft(typeName, "*") 38 | 39 | result.Imports = append(result.Imports, Import{Alias: depName, Path: pack}) 40 | result.Types[i] = fmt.Sprintf("%s%s.%s", strings.Repeat("*", len(typeName)-len(trimmedTypeName)), depName, trimmedTypeName) 41 | } 42 | } 43 | 44 | return result 45 | } 46 | -------------------------------------------------------------------------------- /ast_rewriter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "regexp" 7 | ) 8 | 9 | var ( 10 | letters = []string{ 11 | "T", 12 | "U", 13 | "V", 14 | "W", 15 | "X", 16 | "Y", 17 | "Z", 18 | } 19 | replaceRegexp = regexp.MustCompile(`\b[A-Z]\b`) 20 | ) 21 | 22 | type ASTModifier struct { 23 | mapping map[string]int 24 | parameters *Params 25 | } 26 | 27 | func NewASTModifier(parameters *Params) (*ASTModifier, error) { 28 | result := &ASTModifier{ 29 | mapping: make(map[string]int), 30 | parameters: parameters, 31 | } 32 | 33 | for i := 0; i < len(parameters.Types); i++ { 34 | if i >= len(letters) { 35 | return nil, fmt.Errorf("Too many parameters given, maximum of %d accepted", len(letters)) 36 | } 37 | 38 | result.mapping[letters[i]] = i 39 | } 40 | 41 | return result, nil 42 | } 43 | 44 | func (a *ASTModifier) Visit(node ast.Node) ast.Visitor { 45 | switch n := node.(type) { 46 | case *ast.Ident: 47 | if i, ok := a.mapping[n.Name]; ok { 48 | n.Name = a.parameters.Types[i] 49 | } 50 | case *ast.ImportSpec: 51 | n.Path.Value = replaceRegexp.ReplaceAllStringFunc(n.Path.Value, func(s string) string { 52 | if i, ok := a.mapping[s]; ok { 53 | return a.parameters.RawTypes[i] 54 | } else { 55 | return s 56 | } 57 | }) 58 | } 59 | 60 | return a 61 | } 62 | 63 | // Note: modifies the ast in-place 64 | func rewriteAst(tree *ast.File, parameters *Params) error { 65 | astModifier, err := NewASTModifier(parameters) 66 | if err != nil { 67 | return err 68 | } 69 | ast.Walk(astModifier, tree) 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /templates/f/functional.go: -------------------------------------------------------------------------------- 1 | type FilterFunction func(T) bool 2 | type Function func(T) T 3 | type Function2 func(T, T) T 4 | 5 | func combine(a Function, b Function) Function { 6 | return func(arg T) T { 7 | return a(b(arg)) 8 | } 9 | } 10 | 11 | func Compose(first Function, f ...Function) Function { 12 | for _, v := range f { 13 | first = combine(first, v) 14 | } 15 | return first 16 | } 17 | 18 | func Map(f Function, input []T) []T { 19 | result := make([]T, len(input)) 20 | 21 | for i, element := range input { 22 | result[i] = f(element) 23 | } 24 | 25 | return result 26 | } 27 | 28 | func Reduce(f Function2, input []T) (accum T) { 29 | if len(input) == 0 { 30 | return accum 31 | } 32 | 33 | accum = input[0] 34 | for _, v := range input[1:] { 35 | accum = f(accum, v) 36 | } 37 | 38 | return 39 | } 40 | 41 | func Filter(f FilterFunction, input []T) (result []T) { 42 | for _, v := range input { 43 | if f(v) { 44 | result = append(result, v) 45 | } 46 | } 47 | return 48 | } 49 | 50 | func TakeWhile(f FilterFunction, input []T) (result []T) { 51 | for _, v := range input { 52 | if !f(v) { 53 | break 54 | } 55 | result = append(result, v) 56 | } 57 | return 58 | } 59 | 60 | func DropWhile(f FilterFunction, input []T) (result []T) { 61 | take := false 62 | for _, v := range input { 63 | if take = take || f(v); take { 64 | result = append(result, v) 65 | } 66 | } 67 | return 68 | } 69 | 70 | func ZipWith(f Function2, inputa []T, inputb []T) (result []T) { 71 | upto := len(inputa) 72 | if len(inputb) < upto { 73 | upto = len(inputb) 74 | } 75 | 76 | result = make([]T, upto) 77 | 78 | for i := 0; i < upto; i++ { 79 | result[i] = f(inputa[i], inputb[i]) 80 | } 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /template_params_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | inputs = []struct { 10 | Params, RepoName string 11 | Expected *Params 12 | }{ 13 | { 14 | "string", 15 | &Params{ 16 | Types: []string{"string"}, 17 | RawTypes: []string{"string"}, 18 | }, 19 | }, 20 | { 21 | "*string", 22 | &Params{ 23 | Types: []string{"*string"}, 24 | RawTypes: []string{"*string"}, 25 | }, 26 | }, 27 | { 28 | "string_int", 29 | &Params{ 30 | Types: []string{"string", "int"}, 31 | RawTypes: []string{"string", "int"}, 32 | }, 33 | }, 34 | { 35 | "string_io.Reader", 36 | &Params{ 37 | Types: []string{"string", "dep1.Reader"}, 38 | RawTypes: []string{"string", "io.Reader"}, 39 | Imports: []Import{{Alias: "dep1", Path: "io"}}, 40 | }, 41 | }, 42 | { 43 | "string_io.Reader_net/http.Client", 44 | &Params{ 45 | Types: []string{"string", "dep1.Reader", "dep2.Client"}, 46 | RawTypes: []string{"string", "io.Reader", "net/http.Client"}, 47 | Imports: []Import{{Alias: "dep1", Path: "io"}, {Alias: "dep2", Path: "net/http"}}, 48 | }, 49 | }, 50 | { 51 | "string_gonerics.io/d/set/string/wow.git.Set", 52 | &Params{ 53 | Types: []string{"string", "dep1.Set"}, 54 | RawTypes: []string{"string", "gonerics.io/d/set/string/wow.git.Set"}, 55 | Imports: []Import{{Alias: "dep1", Path: "gonerics.io/d/set/string/wow.git"}}, 56 | }, 57 | }, 58 | { 59 | "string_gonerics.io/d/set/string/wow.git.**Set", 60 | &Params{ 61 | Types: []string{"string", "**dep1.Set"}, 62 | RawTypes: []string{"string", "gonerics.io/d/set/string/wow.git.**Set"}, 63 | Imports: []Import{{Alias: "dep1", Path: "gonerics.io/d/set/string/wow.git"}}, 64 | }, 65 | }, 66 | } 67 | ) 68 | 69 | func TestParseParams(t *testing.T) { 70 | for _, test := range inputs { 71 | result := Parse(test.Param) 72 | 73 | if !reflect.DeepEqual(test.Expected, result) { 74 | t.Error("Failed", test.Params, test.Expected, result) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gonerics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "github.com/google/go-github/github" 7 | "go/parser" 8 | "go/printer" 9 | "go/token" 10 | "io" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | "text/template" 17 | ) 18 | 19 | const ( 20 | headerTemplateSource = `package {{.Package}} 21 | {{range .Imports}}import {{.Alias}} "{{.Path}}" 22 | {{end}} 23 | ` 24 | PARAMETER_DIVIDER = "_" 25 | ) 26 | 27 | var ( 28 | parameters, templateType, name, templatesDir string 29 | 30 | githubClient = github.NewClient(&http.Client{}) 31 | headerTemplate = template.New("HeaderTemplate") 32 | 33 | gist *github.Gist 34 | gistFile github.GistFile 35 | ) 36 | 37 | func init() { 38 | flag.StringVar(¶meters, "parameters", "", "The parameters int_string_io.Reader") 39 | flag.StringVar(&templateType, "template-type", "", "The template type d/f/g") 40 | flag.StringVar(&name, "name", "", "The name of the template, map, graph, set") 41 | flag.StringVar(&templatesDir, "templates-dir", "", "The directory that contains the templates") 42 | 43 | headerTemplate.Parse(headerTemplateSource) 44 | } 45 | 46 | func main() { 47 | log.SetOutput(os.Stderr) 48 | flag.Parse() 49 | 50 | if parameters == "" || templateType == "" || name == "" { 51 | flag.PrintDefaults() 52 | os.Exit(1) 53 | } 54 | 55 | if templatesDir == "" { 56 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 57 | if err != nil { 58 | log.Fatal("Failed to get directory", err) 59 | } 60 | 61 | templatesDir = filepath.Join(dir, "templates") 62 | } 63 | 64 | buffer := new(bytes.Buffer) 65 | 66 | templateParameters := Parse(parameters) 67 | 68 | fset := token.NewFileSet() 69 | 70 | switch templateType { 71 | case "f", "d": 72 | templateParameters.Package = name 73 | case "g": 74 | gist, _, err := githubClient.Gists.Get(name) 75 | 76 | if err != nil { 77 | log.Fatal("Failed to get gist ", err) 78 | } 79 | 80 | if len(gist.Files) != 1 { 81 | log.Fatal("Need exactly one file") 82 | } 83 | var key github.GistFilename 84 | for key, gistFile = range gist.Files { 85 | break 86 | } 87 | 88 | if !strings.HasSuffix(string(key), ".go") { 89 | log.Fatal("The filename needs to end with .go") 90 | } 91 | 92 | templateParameters.Package = string(key[:len(key)-3]) 93 | default: 94 | log.Fatal("Unknown template type ", templateType) 95 | } 96 | 97 | headerTemplate.Execute(buffer, templateParameters) 98 | 99 | switch templateType { 100 | case "f", "d": 101 | filename := filepath.Join(templatesDir, templateType, name+".go") 102 | log.Printf("Using %s as template", filename) 103 | file, err := os.Open(filename) 104 | if err != nil { 105 | log.Fatal("Failed to open template file", err) 106 | } 107 | io.Copy(buffer, file) 108 | file.Close() 109 | case "g": 110 | buffer.WriteString(*gistFile.Content) 111 | } 112 | 113 | tree, err := parser.ParseFile(fset, "file.go", buffer, 0) 114 | if err != nil { 115 | log.Fatal("Error while parsing template", err) 116 | } 117 | 118 | if err = rewriteAst(tree, templateParameters); err != nil { 119 | log.Fatal("Error while rewriting AST", err) 120 | } 121 | printer.Fprint(os.Stdout, fset, tree) 122 | } 123 | --------------------------------------------------------------------------------