Skip to content

Commit ee01a95

Browse files
authored
Add initial support for tuple comparison (#9)
1 parent f5ae5e8 commit ee01a95

26 files changed

+3320
-43
lines changed

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,100 @@ tup := tuple.New2(5, "hi!")
5757
a, b := tup.Values()
5858
```
5959

60+
## Comparison
61+
62+
Tuples are compared from the first element to the last.
63+
For example, the tuple `[1 2 3]` is greater than `[1 2 4]` but less than `[2 2 2]`.
64+
65+
```go
66+
fmt.Println(tuple.Equal3(tuple.New3(1, 2, 3), tuple.New3(3, 3, 3))) // false.
67+
fmt.Println(tuple.LessThan3(tuple.New3(1, 2, 3), tuple.New3(3, 2, 1))) // true.
68+
69+
tups := []tuple.T3{
70+
tuple.New3("foo", 2, -23),
71+
tuple.New3("foo", 72, 15),
72+
tuple.New3("bar", -4, 43),
73+
}
74+
sort.Slice(tups, func (i, j int) {
75+
return tuple.LessThan3(tups[i], tups[j])
76+
})
77+
78+
fmt.Println(tups) // [["bar", -4, 43], ["foo", 2, -23], ["foo", 72, 15]].
79+
```
80+
81+
---
82+
**NOTE**
83+
84+
In order to compare tuples, all tuple elements must match `constraints.Ordered`.
85+
86+
See [Custom comparison](#custom-comparison) in order to see how to compare tuples
87+
with arbitrary element values.
88+
89+
---
90+
91+
### Comparison result
92+
93+
```go
94+
// Compare* functions return an OrderedComparisonResult value.
95+
result := tuple.Compare3(tuple.New3(1, 2, 3), tuple.New3(3, 2, 1))
96+
97+
// OrderedComparisonResult values are wrapped integers.
98+
fmt.Println(result) // -1
99+
100+
// OrderedComparisonResult expose various method to see the result
101+
// in a more readable way.
102+
fmt.Println(result.GreaterThan()) // false
103+
```
104+
105+
### Custom comparison
106+
107+
The package provides the `CompareC` comparison functions varation in order to compare tuples of complex
108+
comparable types.
109+
110+
For a type to be comparable, it must match the `Comparable` or `Equalable` constraints.
111+
112+
```go
113+
type Comparable[T any] interface {
114+
CompareTo(guest T) OrderedComparisonResult
115+
}
116+
117+
type Equalable[T any] interface {
118+
Equal(guest T) bool
119+
}
120+
```
121+
122+
```go
123+
type person struct {
124+
name string
125+
age int
126+
}
127+
128+
func (p person) CompareTo(guest person) tuple.OrderedComparisonResult {
129+
if p.name < guest.name {
130+
return -1
131+
}
132+
if p.name > guest.name {
133+
return 1
134+
}
135+
return 0
136+
}
137+
138+
func main() {
139+
tup1 := tuple.New2(person{name: "foo", age: 20}, person{name: "bar", age: 24})
140+
tup2 := tuple.New2(person{name: "bar", age: 20}, person{name: "baz", age: 24})
141+
142+
fmt.Println(tuple.LessThan2C(tup1, tup2)) // true.
143+
}
144+
```
145+
146+
In order to call the complex types variation of the comparable functions, __all__ tuple types must match the `Comparable` constraint.
147+
148+
While this is not ideal, this a known inconvenience given the current type parameters capabilities in Go.
149+
Some solutions have been porposed for this issue ([lesser](https://github.com/lelysses/lesser), for example, beatifully articulates the issue),
150+
but they still demand features that are not yet implemented by the language.
151+
152+
Once the language will introduce more convenient ways for generic comparisons, this package will adopt it.
153+
60154
## Formatting
61155

62156
Tuples implement the `Stringer` and `GoStringer` interfaces.

comparison.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package tuple
2+
3+
import (
4+
"constraints"
5+
)
6+
7+
// OrderedComparisonResult represents the result of a tuple ordered comparison.
8+
// OrderedComparisonResult == 0 represents that the tuples are equal.
9+
// OrderedComparisonResult < 0 represent that the host tuple is less than the guest tuple.
10+
// OrderedComparisonResult > 0 represent that the host tuple is greater than the guest tuple.
11+
type OrderedComparisonResult int
12+
13+
// Comparable is a constraint interface for complex tuple elements that can be compared to other instances.
14+
// In order to compare tuples, either all of their elements must be Ordered, or Comparable.
15+
type Comparable[T any] interface {
16+
CompareTo(guest T) OrderedComparisonResult
17+
}
18+
19+
// Equalable is a constraint interface for complex tuple elements whose equality to other instances can be tested.
20+
type Equalable[T any] interface {
21+
Equal(guest T) bool
22+
}
23+
24+
// Equal returns whether the compared values are equal.
25+
func (result OrderedComparisonResult) Equal() bool {
26+
return result == 0
27+
}
28+
29+
// LessThan returns whether the host is less than the guest.
30+
func (result OrderedComparisonResult) LessThan() bool {
31+
return result < 0
32+
}
33+
34+
// LessOrEqual returns whether the host is less than or equal to the guest.
35+
func (result OrderedComparisonResult) LessOrEqual() bool {
36+
return result <= 0
37+
}
38+
39+
// GreaterThan returns whether the host is greater than the guest.
40+
func (result OrderedComparisonResult) GreaterThan() bool {
41+
return result > 0
42+
}
43+
44+
// GreaterOrEqual returns whether the host is greater than or equal to the guest.
45+
func (result OrderedComparisonResult) GreaterOrEqual() bool {
46+
return result >= 0
47+
}
48+
49+
// EQ is short for Equal and returns whether the compared values are equal.
50+
func (result OrderedComparisonResult) EQ() bool {
51+
return result.Equal()
52+
}
53+
54+
// LT is short for LessThan and returns whether the host is less than the guest.
55+
func (result OrderedComparisonResult) LT() bool {
56+
return result.LessThan()
57+
}
58+
59+
// LE is short for LessOrEqual and returns whether the host is less than or equal to the guest.
60+
func (result OrderedComparisonResult) LE() bool {
61+
return result.LessOrEqual()
62+
}
63+
64+
// GT is short for GreaterThan and returns whether the host is greater than the guest.
65+
func (result OrderedComparisonResult) GT() bool {
66+
return result.GreaterThan()
67+
}
68+
69+
// GE is short for GreaterOrEqual and returns whether the host is greater than or equal to the guest.
70+
func (result OrderedComparisonResult) GE() bool {
71+
return result.GreaterOrEqual()
72+
}
73+
74+
// multiCompare calls and compares the predicates by order.
75+
// multiCompare will short-circuit once one of the predicates returns a non-equal result, and the rest
76+
// of the predicates will not be called.
77+
func multiCompare(predicates ...func() OrderedComparisonResult) OrderedComparisonResult {
78+
for _, pred := range predicates {
79+
if result := pred(); !result.Equal() {
80+
return result
81+
}
82+
}
83+
84+
return 0
85+
}
86+
87+
// compareOrdered returns the comparison result between the host and guest values provided they match the Ordered constraint.
88+
func compareOrdered[T constraints.Ordered](host, guest T) OrderedComparisonResult {
89+
if host < guest {
90+
return -1
91+
}
92+
if host > guest {
93+
return 1
94+
}
95+
96+
return 0
97+
}

comparison_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tuple
2+
3+
// approximationHelper is a helper type for testing type approximation.
4+
type approximationHelper string
5+
6+
// intEqualable is a wrapper type for int that implements the Equalable constraint.
7+
type intEqualable int
8+
9+
// stringComparable is a wrapper type for string that implements the Comparable constraint.
10+
type stringComparable string
11+
12+
// Assert implementation.
13+
var _ Equalable[intEqualable] = (intEqualable)(0)
14+
var _ Comparable[stringComparable] = (stringComparable)("")
15+
16+
func (i intEqualable) Equal(other intEqualable) bool {
17+
return i == other
18+
}
19+
20+
func (s stringComparable) CompareTo(other stringComparable) OrderedComparisonResult {
21+
return compareOrdered(s, other)
22+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ module github.com/barweiss/go-tuple
22

33
go 1.18
44

5+
require github.com/stretchr/testify v1.7.0
6+
57
require (
68
github.com/davecgh/go-spew v1.1.0 // indirect
79
github.com/pmezard/go-difflib v1.0.0 // indirect
810
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
9-
github.com/stretchr/testify v1.7.0
1011
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
55
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
66
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
77
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
89
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
910
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
1011
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

scripts/gen/main.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ type templateContext struct {
1616
Len int
1717
TypeName string
1818
TypeDecl string
19-
GenericTypesDecl string
2019
GenericTypesForward string
2120
}
2221

@@ -27,6 +26,23 @@ var funcMap = template.FuncMap{
2726
"quote": func(value interface{}) string {
2827
return fmt.Sprintf("%q", fmt.Sprint(value))
2928
},
29+
"inc": func(value int) int {
30+
return value + 1
31+
},
32+
"typeRef": func(indexes []int, suffix ...string) string {
33+
if len(suffix) > 1 {
34+
panic(fmt.Errorf("typeRef accepts at most 1 suffix argument"))
35+
}
36+
37+
var typeNameSuffix string
38+
if len(suffix) == 1 {
39+
typeNameSuffix = suffix[0]
40+
}
41+
42+
return fmt.Sprintf("T%d%s[%s]", len(indexes), typeNameSuffix, genTypesForward(indexes))
43+
},
44+
"genericTypesDecl": genTypesDecl,
45+
"genericTypesDeclGenericConstraint": genTypesDeclGenericConstraint,
3046
}
3147

3248
//go:embed tuple.tpl
@@ -55,15 +71,10 @@ func main() {
5571
indexes[index] = index + 1
5672
}
5773

58-
decl := genTypesDecl(indexes)
59-
forward := genTypesForward(indexes)
6074
context := templateContext{
6175
Indexes: indexes,
6276
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,
77+
GenericTypesForward: genTypesForward(indexes),
6778
}
6879

6980
filesToGenerate := []struct {
@@ -112,18 +123,25 @@ func generateFile(context templateContext, outputFilePath string, tpl *template.
112123
}
113124
}
114125

126+
func genTypesDeclGenericConstraint(indexes []int, constraint string) string {
127+
sep := make([]string, len(indexes))
128+
for index, typeIndex := range indexes {
129+
typ := fmt.Sprintf("Ty%d", typeIndex)
130+
sep[index] = fmt.Sprintf("%s %s[%s]", typ, constraint, typ)
131+
}
132+
133+
return strings.Join(sep, ", ")
134+
}
135+
115136
// genTypesDecl generates a "TypeParamDecl" (https://tip.golang.org/ref/spec#Type_parameter_lists) expression,
116137
// used to declare generic types for a type or a function, according to the given element indexes.
117-
func genTypesDecl(indexes []int) string {
138+
func genTypesDecl(indexes []int, constraint string) string {
118139
sep := make([]string, len(indexes))
119140
for index, typeIndex := range indexes {
120141
sep[index] = fmt.Sprintf("Ty%d", typeIndex)
121142
}
122143

123-
// Add constraint to last element.
124-
sep[len(indexes)-1] += " any"
125-
126-
return strings.Join(sep, ", ")
144+
return strings.Join(sep, ", ") + " " + constraint
127145
}
128146

129147
// genTypesForward generates a "TypeParamList" (https://tip.golang.org/ref/spec#Type_parameter_lists) expression,

0 commit comments

Comments
 (0)