Skip to content

Commit bb7b744

Browse files
committed
Add initial tuple implementation
1 parent 16fc8b5 commit bb7b744

28 files changed

+5055
-2
lines changed

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,56 @@
1-
# go-tuple
2-
Go 1.18 generic tuples
1+
# go-tuple: Generic tuples for Go 1.18.
2+
3+
Go 1.18 tuple implementation.
4+
5+
Use tuples to store 1 or more values without needing to write a custom struct.
6+
7+
```go
8+
tup := tuple.New2(5, "hi!")
9+
fmt.Println(tup.V1) // Outputs 5.
10+
fmt.Println(tup.V2) // Outputs "hi!".
11+
```
12+
13+
Tuples come in various sizes, from 1 to 9 elements.
14+
15+
```go
16+
longerTuple := tuple.New5("this", "is", "one", "long", "tuple")
17+
```
18+
19+
Tuples can be used as slice or array items, map keys or values, and as channel payloads.
20+
21+
# Features
22+
23+
## Create tuples from function calls
24+
25+
```go
26+
func vals() (int, string) {
27+
return 5, "hi!"
28+
}
29+
30+
func main() {
31+
tup := tuple.New2(vals())
32+
fmt.Println(tup.V1)
33+
fmt.Println(tup.V2)
34+
}
35+
```
36+
37+
## Forward tuples as function arguments
38+
39+
```go
40+
func main() {
41+
tup := tuple.New2(5, "hi!")
42+
printValues(tup.Values())
43+
}
44+
45+
func printValues(a int, b string) {
46+
fmt.Println(a)
47+
fmt.Println(b)
48+
}
49+
```
50+
51+
## Access tuple values
52+
53+
```go
54+
tup := tuple.New2(5, "hi!")
55+
a, b := tup.Values()
56+
```

generate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package tuple
2+
3+
//go:generate go run scripts/gen/main.go .

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/barweiss/go-tuple
2+
3+
go 1.18
4+
5+
require (
6+
github.com/davecgh/go-spew v1.1.0 // indirect
7+
github.com/pmezard/go-difflib v1.0.0 // indirect
8+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
9+
github.com/stretchr/testify v1.7.0
10+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

pair.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package tuple
2+
3+
// Pair is a generic type that holds two values.
4+
type Pair[Ty1 any, Ty2 any] T2[Ty1, Ty2]

scripts/gen/main.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path"
9+
"strings"
10+
"text/template"
11+
)
12+
13+
// templateContext is the context passed to the template engine for generating tuple code and test files.
14+
type templateContext struct {
15+
Indexes []int
16+
Len int
17+
TypeName string
18+
TypeDecl string
19+
GenericTypesDecl string
20+
GenericTypesForward string
21+
}
22+
23+
const minTupleLength = 1
24+
const maxTupleLength = 9
25+
26+
var funcMap = template.FuncMap{
27+
"quote": func(value interface{}) string {
28+
return fmt.Sprintf("%q", fmt.Sprint(value))
29+
},
30+
}
31+
32+
//go:embed tuple.tpl
33+
var codeTplContent string
34+
35+
//go:embed tuple_test.tpl
36+
var testTplContent string
37+
38+
// main generates the tuple code and test files by executing the template engine for the "tuple.tpl" and "tuple_test.tpl" files.
39+
func main() {
40+
outputDir := os.Args[1]
41+
42+
codeTpl, err := template.New("tuple").Funcs(funcMap).Parse(codeTplContent)
43+
if err != nil {
44+
panic(err)
45+
}
46+
47+
testTpl, err := template.New("tuple_test").Funcs(funcMap).Parse(testTplContent)
48+
if err != nil {
49+
panic(err)
50+
}
51+
52+
for tupleLength := minTupleLength; tupleLength <= maxTupleLength; tupleLength++ {
53+
indexes := make([]int, tupleLength)
54+
for index := range indexes {
55+
indexes[index] = index + 1
56+
}
57+
58+
decl := genTypesDecl(indexes)
59+
forward := genTypesForward(indexes)
60+
context := templateContext{
61+
Indexes: indexes,
62+
Len: tupleLength,
63+
TypeName: fmt.Sprintf("T%d[%s]", tupleLength, forward),
64+
TypeDecl: fmt.Sprintf("T%d[%s]", tupleLength, decl),
65+
GenericTypesDecl: decl,
66+
GenericTypesForward: forward,
67+
}
68+
69+
filesToGenerate := []struct {
70+
fullPath string
71+
tpl *template.Template
72+
}{
73+
{
74+
fullPath: path.Join(outputDir, fmt.Sprintf("tuple%d.go", tupleLength)),
75+
tpl: codeTpl,
76+
},
77+
{
78+
fullPath: path.Join(outputDir, fmt.Sprintf("tuple%d_test.go", tupleLength)),
79+
tpl: testTpl,
80+
},
81+
}
82+
83+
for _, file := range filesToGenerate {
84+
fmt.Printf("Generating file %q...\n", file.fullPath)
85+
generateFile(context, file.fullPath, file.tpl)
86+
}
87+
}
88+
}
89+
90+
// generateFile generates the file at outputFilePath according to the template tpl.
91+
// The template engine is given the context parameter as data (can be used as "." in the templates).
92+
func generateFile(context templateContext, outputFilePath string, tpl *template.Template) {
93+
file, err := os.OpenFile(outputFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
94+
if err != nil {
95+
panic(err)
96+
}
97+
98+
defer func() {
99+
if err := file.Close(); err != nil {
100+
panic(err)
101+
}
102+
}()
103+
104+
err = tpl.Execute(file, context)
105+
if err != nil {
106+
panic(err)
107+
}
108+
109+
cmd := exec.Command("gofmt", "-s", "-w", outputFilePath)
110+
if err := cmd.Run(); err != nil {
111+
panic(err)
112+
}
113+
}
114+
115+
// genTypesDecl generates a "TypeParamDecl" (https://tip.golang.org/ref/spec#Type_parameter_lists) expression,
116+
// used to declare generic types for a type or a function, according to the given element indexes.
117+
func genTypesDecl(indexes []int) string {
118+
sep := make([]string, len(indexes))
119+
for index, typeIndex := range indexes {
120+
sep[index] = fmt.Sprintf("Ty%d any", typeIndex)
121+
}
122+
123+
return strings.Join(sep, ", ")
124+
}
125+
126+
// genTypesForward generates a "TypeParamList" (https://tip.golang.org/ref/spec#Type_parameter_lists) expression,
127+
// used to instantiate generic classes, according to the given element indexes.
128+
// Forward refers to forwarding already declared type parameters in order to instantiate the type.
129+
func genTypesForward(indexes []int) string {
130+
sep := make([]string, len(indexes))
131+
for index, typeIndex := range indexes {
132+
sep[index] = fmt.Sprintf("Ty%d", typeIndex)
133+
}
134+
135+
return strings.Join(sep, ", ")
136+
}

scripts/gen/tuple.tpl

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package tuple
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
{{/* $typeName can be used when the context of dot changes. */}}
8+
{{$typeName := .TypeName}}
9+
10+
// T{{.Len}} is a tuple type holding {{.Len}} generic values.
11+
type T{{.Len}}[{{.GenericTypesDecl}}] struct {
12+
{{range .Indexes -}}
13+
V{{.}} Ty{{.}}
14+
{{end -}}
15+
}
16+
17+
// Len returns the number of values held by the tuple.
18+
func (t {{.TypeName}}) Len() int {
19+
return {{.Len}}
20+
}
21+
22+
// Values returns the values held by the tuple.
23+
func (t {{.TypeName}}) Values() ({{.GenericTypesForward}}) {
24+
return {{range $index, $num := .Indexes -}}
25+
{{- if gt $index 0}}, {{end -}}
26+
t.V{{$num}}
27+
{{- end}}
28+
}
29+
30+
// Array returns an array of the tuple values.
31+
func (t {{.TypeName}}) Array() [{{.Len}}]any {
32+
return [{{.Len}}]any{
33+
{{ range .Indexes -}}
34+
t.V{{.}},
35+
{{end}}
36+
}
37+
}
38+
39+
// Slice returns a slice of the tuple values.
40+
func (t {{.TypeName}}) Slice() []any {
41+
a := t.Array()
42+
return a[:]
43+
}
44+
45+
// String returns the string representation of the tuple.
46+
func (t {{.TypeName}}) String() string {
47+
return tupString(t.Slice())
48+
}
49+
50+
// GoString returns a Go-syntax representation of the tuple.
51+
func (t {{.TypeName}}) GoString() string {
52+
return tupGoString(t.Slice())
53+
}
54+
55+
// New{{.Len}} creates a new tuple holding {{.Len}} generic values.
56+
func New{{.Len}}[{{.GenericTypesDecl}}](
57+
{{- range $index, $num := .Indexes -}}
58+
{{- if gt $index 0}}, {{end -}}
59+
v{{.}} Ty{{.}}
60+
{{- end -}}
61+
) {{.TypeName}} {
62+
return {{.TypeName}}{
63+
{{range .Indexes -}}
64+
V{{.}}: v{{.}},
65+
{{end}}
66+
}
67+
}
68+
69+
// FromArray{{.Len}} returns a tuple from an array of length {{.Len}}.
70+
// If any of the values can not be converted to the generic type, an error is returned.
71+
func FromArray{{.Len}}[{{.GenericTypesDecl}}](arr [{{.Len}}]any) ({{.TypeName}}, error) {
72+
{{range $index, $num := .Indexes -}}
73+
v{{$num}}, ok := arr[{{$index}}].(Ty{{$num}})
74+
if !ok {
75+
return {{$typeName}}{}, fmt.Errorf("value at array index {{$index}} expected to have type %s but has type %T", typeName[Ty{{$num}}](), arr[{{$index}}])
76+
}
77+
{{end}}
78+
return New{{.Len}}(
79+
{{- range $index, $num := .Indexes -}}
80+
{{- if gt $index 0}}, {{end -}}
81+
v{{$num}}
82+
{{- end -}}
83+
), nil
84+
}
85+
86+
// FromArray{{.Len}}X returns a tuple from an array of length {{.Len}}.
87+
// If any of the values can not be converted to the generic type, the function panics.
88+
func FromArray{{.Len}}X[{{.GenericTypesDecl}}](arr [{{.Len}}]any) {{.TypeName}} {
89+
return FromSlice{{.Len}}X[{{.GenericTypesForward}}](arr[:])
90+
}
91+
92+
// FromSlice{{.Len}} returns a tuple from a slice of length {{.Len}}.
93+
// If the length of the slice doesn't match, or any of the values can not be converted to the generic type, an error is returned.
94+
func FromSlice{{.Len}}[{{.GenericTypesDecl}}](values []any) ({{.TypeName}}, error) {
95+
if len(values) != {{.Len}} {
96+
return {{.TypeName}}{}, fmt.Errorf("slice length %d must match number of tuple values {{.Len}}", len(values))
97+
}
98+
99+
{{range $index, $num := .Indexes -}}
100+
v{{$num}}, ok := values[{{$index}}].(Ty{{$num}})
101+
if !ok {
102+
return {{$typeName}}{}, fmt.Errorf("value at slice index {{$index}} expected to have type %s but has type %T", typeName[Ty{{$num}}](), values[{{$index}}])
103+
}
104+
{{end}}
105+
return New{{.Len}}(
106+
{{- range $index, $num := .Indexes -}}
107+
{{- if gt $index 0}}, {{end -}}
108+
v{{$num}}
109+
{{- end -}}
110+
), nil
111+
}
112+
113+
// FromSlice{{.Len}}X returns a tuple from a slice of length {{.Len}}.
114+
// If the length of the slice doesn't match, or any of the values can not be converted to the generic type, the function panics.
115+
func FromSlice{{.Len}}X[{{.GenericTypesDecl}}](values []any) {{.TypeName}} {
116+
if len(values) != {{.Len}} {
117+
panic(fmt.Errorf("slice length %d must match number of tuple values {{.Len}}", len(values)))
118+
}
119+
120+
{{range $index, $num := .Indexes -}}
121+
v{{$num}} := values[{{$index}}].(Ty{{$num}})
122+
{{end}}
123+
return New{{.Len}}(
124+
{{- range $index, $num := .Indexes -}}
125+
{{- if gt $index 0}}, {{end -}}
126+
v{{$num}}
127+
{{- end -}}
128+
)
129+
}

0 commit comments

Comments
 (0)