Skip to content

Commit

Permalink
Add initial support for tuple comparison (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
barweiss authored Jan 1, 2022
1 parent f5ae5e8 commit ee01a95
Show file tree
Hide file tree
Showing 26 changed files with 3,320 additions and 43 deletions.
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,100 @@ tup := tuple.New2(5, "hi!")
a, b := tup.Values()
```

## Comparison

Tuples are compared from the first element to the last.
For example, the tuple `[1 2 3]` is greater than `[1 2 4]` but less than `[2 2 2]`.

```go
fmt.Println(tuple.Equal3(tuple.New3(1, 2, 3), tuple.New3(3, 3, 3))) // false.
fmt.Println(tuple.LessThan3(tuple.New3(1, 2, 3), tuple.New3(3, 2, 1))) // true.

tups := []tuple.T3{
tuple.New3("foo", 2, -23),
tuple.New3("foo", 72, 15),
tuple.New3("bar", -4, 43),
}
sort.Slice(tups, func (i, j int) {
return tuple.LessThan3(tups[i], tups[j])
})

fmt.Println(tups) // [["bar", -4, 43], ["foo", 2, -23], ["foo", 72, 15]].
```

---
**NOTE**

In order to compare tuples, all tuple elements must match `constraints.Ordered`.

See [Custom comparison](#custom-comparison) in order to see how to compare tuples
with arbitrary element values.

---

### Comparison result

```go
// Compare* functions return an OrderedComparisonResult value.
result := tuple.Compare3(tuple.New3(1, 2, 3), tuple.New3(3, 2, 1))

// OrderedComparisonResult values are wrapped integers.
fmt.Println(result) // -1

// OrderedComparisonResult expose various method to see the result
// in a more readable way.
fmt.Println(result.GreaterThan()) // false
```

### Custom comparison

The package provides the `CompareC` comparison functions varation in order to compare tuples of complex
comparable types.

For a type to be comparable, it must match the `Comparable` or `Equalable` constraints.

```go
type Comparable[T any] interface {
CompareTo(guest T) OrderedComparisonResult
}

type Equalable[T any] interface {
Equal(guest T) bool
}
```

```go
type person struct {
name string
age int
}

func (p person) CompareTo(guest person) tuple.OrderedComparisonResult {
if p.name < guest.name {
return -1
}
if p.name > guest.name {
return 1
}
return 0
}

func main() {
tup1 := tuple.New2(person{name: "foo", age: 20}, person{name: "bar", age: 24})
tup2 := tuple.New2(person{name: "bar", age: 20}, person{name: "baz", age: 24})

fmt.Println(tuple.LessThan2C(tup1, tup2)) // true.
}
```

In order to call the complex types variation of the comparable functions, __all__ tuple types must match the `Comparable` constraint.

While this is not ideal, this a known inconvenience given the current type parameters capabilities in Go.
Some solutions have been porposed for this issue ([lesser](https://github.com/lelysses/lesser), for example, beatifully articulates the issue),
but they still demand features that are not yet implemented by the language.

Once the language will introduce more convenient ways for generic comparisons, this package will adopt it.

## Formatting

Tuples implement the `Stringer` and `GoStringer` interfaces.
Expand Down
97 changes: 97 additions & 0 deletions comparison.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tuple

import (
"constraints"
)

// OrderedComparisonResult represents the result of a tuple ordered comparison.
// OrderedComparisonResult == 0 represents that the tuples are equal.
// OrderedComparisonResult < 0 represent that the host tuple is less than the guest tuple.
// OrderedComparisonResult > 0 represent that the host tuple is greater than the guest tuple.
type OrderedComparisonResult int

// Comparable is a constraint interface for complex tuple elements that can be compared to other instances.
// In order to compare tuples, either all of their elements must be Ordered, or Comparable.
type Comparable[T any] interface {
CompareTo(guest T) OrderedComparisonResult
}

// Equalable is a constraint interface for complex tuple elements whose equality to other instances can be tested.
type Equalable[T any] interface {
Equal(guest T) bool
}

// Equal returns whether the compared values are equal.
func (result OrderedComparisonResult) Equal() bool {
return result == 0
}

// LessThan returns whether the host is less than the guest.
func (result OrderedComparisonResult) LessThan() bool {
return result < 0
}

// LessOrEqual returns whether the host is less than or equal to the guest.
func (result OrderedComparisonResult) LessOrEqual() bool {
return result <= 0
}

// GreaterThan returns whether the host is greater than the guest.
func (result OrderedComparisonResult) GreaterThan() bool {
return result > 0
}

// GreaterOrEqual returns whether the host is greater than or equal to the guest.
func (result OrderedComparisonResult) GreaterOrEqual() bool {
return result >= 0
}

// EQ is short for Equal and returns whether the compared values are equal.
func (result OrderedComparisonResult) EQ() bool {
return result.Equal()
}

// LT is short for LessThan and returns whether the host is less than the guest.
func (result OrderedComparisonResult) LT() bool {
return result.LessThan()
}

// LE is short for LessOrEqual and returns whether the host is less than or equal to the guest.
func (result OrderedComparisonResult) LE() bool {
return result.LessOrEqual()
}

// GT is short for GreaterThan and returns whether the host is greater than the guest.
func (result OrderedComparisonResult) GT() bool {
return result.GreaterThan()
}

// GE is short for GreaterOrEqual and returns whether the host is greater than or equal to the guest.
func (result OrderedComparisonResult) GE() bool {
return result.GreaterOrEqual()
}

// multiCompare calls and compares the predicates by order.
// multiCompare will short-circuit once one of the predicates returns a non-equal result, and the rest
// of the predicates will not be called.
func multiCompare(predicates ...func() OrderedComparisonResult) OrderedComparisonResult {
for _, pred := range predicates {
if result := pred(); !result.Equal() {
return result
}
}

return 0
}

// compareOrdered returns the comparison result between the host and guest values provided they match the Ordered constraint.
func compareOrdered[T constraints.Ordered](host, guest T) OrderedComparisonResult {
if host < guest {
return -1
}
if host > guest {
return 1
}

return 0
}
22 changes: 22 additions & 0 deletions comparison_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tuple

// approximationHelper is a helper type for testing type approximation.
type approximationHelper string

// intEqualable is a wrapper type for int that implements the Equalable constraint.
type intEqualable int

// stringComparable is a wrapper type for string that implements the Comparable constraint.
type stringComparable string

// Assert implementation.
var _ Equalable[intEqualable] = (intEqualable)(0)
var _ Comparable[stringComparable] = (stringComparable)("")

func (i intEqualable) Equal(other intEqualable) bool {
return i == other
}

func (s stringComparable) CompareTo(other stringComparable) OrderedComparisonResult {
return compareOrdered(s, other)
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ module github.com/barweiss/go-tuple

go 1.18

require github.com/stretchr/testify v1.7.0

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
github.com/stretchr/testify v1.7.0
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
42 changes: 30 additions & 12 deletions scripts/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type templateContext struct {
Len int
TypeName string
TypeDecl string
GenericTypesDecl string
GenericTypesForward string
}

Expand All @@ -27,6 +26,23 @@ var funcMap = template.FuncMap{
"quote": func(value interface{}) string {
return fmt.Sprintf("%q", fmt.Sprint(value))
},
"inc": func(value int) int {
return value + 1
},
"typeRef": func(indexes []int, suffix ...string) string {
if len(suffix) > 1 {
panic(fmt.Errorf("typeRef accepts at most 1 suffix argument"))
}

var typeNameSuffix string
if len(suffix) == 1 {
typeNameSuffix = suffix[0]
}

return fmt.Sprintf("T%d%s[%s]", len(indexes), typeNameSuffix, genTypesForward(indexes))
},
"genericTypesDecl": genTypesDecl,
"genericTypesDeclGenericConstraint": genTypesDeclGenericConstraint,
}

//go:embed tuple.tpl
Expand Down Expand Up @@ -55,15 +71,10 @@ func main() {
indexes[index] = index + 1
}

decl := genTypesDecl(indexes)
forward := genTypesForward(indexes)
context := templateContext{
Indexes: indexes,
Len: tupleLength,
TypeName: fmt.Sprintf("T%d[%s]", tupleLength, forward),
TypeDecl: fmt.Sprintf("T%d[%s]", tupleLength, decl),
GenericTypesDecl: decl,
GenericTypesForward: forward,
GenericTypesForward: genTypesForward(indexes),
}

filesToGenerate := []struct {
Expand Down Expand Up @@ -112,18 +123,25 @@ func generateFile(context templateContext, outputFilePath string, tpl *template.
}
}

func genTypesDeclGenericConstraint(indexes []int, constraint string) string {
sep := make([]string, len(indexes))
for index, typeIndex := range indexes {
typ := fmt.Sprintf("Ty%d", typeIndex)
sep[index] = fmt.Sprintf("%s %s[%s]", typ, constraint, typ)
}

return strings.Join(sep, ", ")
}

// genTypesDecl generates a "TypeParamDecl" (https://tip.golang.org/ref/spec#Type_parameter_lists) expression,
// used to declare generic types for a type or a function, according to the given element indexes.
func genTypesDecl(indexes []int) string {
func genTypesDecl(indexes []int, constraint string) string {
sep := make([]string, len(indexes))
for index, typeIndex := range indexes {
sep[index] = fmt.Sprintf("Ty%d", typeIndex)
}

// Add constraint to last element.
sep[len(indexes)-1] += " any"

return strings.Join(sep, ", ")
return strings.Join(sep, ", ") + " " + constraint
}

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

0 comments on commit ee01a95

Please # to comment.