Documentation
¶
Overview ¶
Package eq provides type-safe equality comparisons for any type in Go.
Overview ¶
The eq package implements the Eq type class from functional programming, which represents types that support equality comparison. Unlike Go's built-in == operator which only works with comparable types, Eq allows defining custom equality semantics for any type, including complex structures, functions, and non-comparable types.
Core Concepts ¶
The Eq[T] interface represents an equality predicate for type T:
type Eq[T any] interface {
Equals(x, y T) bool
}
This abstraction enables:
- Custom equality semantics for any type
- Composition of equality predicates
- Contravariant mapping to transform equality predicates
- Monoid structure for combining multiple equality checks
Basic Usage ¶
Creating equality predicates for comparable types:
// For built-in comparable types
intEq := eq.FromStrictEquals[int]()
assert.True(t, intEq.Equals(42, 42))
assert.False(t, intEq.Equals(42, 43))
stringEq := eq.FromStrictEquals[string]()
assert.True(t, stringEq.Equals("hello", "hello"))
Creating custom equality predicates:
// Case-insensitive string equality
caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
// Approximate float equality
approxEq := eq.FromEquals(func(a, b float64) bool {
return math.Abs(a-b) < 0.0001
})
assert.True(t, approxEq.Equals(1.0, 1.00009))
Contramap - Transforming Equality ¶
Contramap allows you to create an equality predicate for type B from an equality predicate for type A, given a function from B to A. This is useful for comparing complex types by extracting comparable fields:
type Person struct {
ID int
Name string
Age int
}
// Compare persons by ID only
personEqByID := eq.Contramap(func(p Person) int {
return p.ID
})(eq.FromStrictEquals[int]())
p1 := Person{ID: 1, Name: "Alice", Age: 30}
p2 := Person{ID: 1, Name: "Bob", Age: 25}
assert.True(t, personEqByID.Equals(p1, p2)) // Same ID
// Compare persons by name
personEqByName := eq.Contramap(func(p Person) string {
return p.Name
})(eq.FromStrictEquals[string]())
assert.False(t, personEqByName.Equals(p1, p2)) // Different names
Semigroup and Monoid ¶
The eq package provides Semigroup and Monoid instances for Eq[A], allowing you to combine multiple equality predicates using logical AND:
type User struct {
Username string
Email string
}
// Compare by username
usernameEq := eq.Contramap(func(u User) string {
return u.Username
})(eq.FromStrictEquals[string]())
// Compare by email
emailEq := eq.Contramap(func(u User) string {
return u.Email
})(eq.FromStrictEquals[string]())
// Combine: users are equal if BOTH username AND email match
userEq := eq.Semigroup[User]().Concat(usernameEq, emailEq)
u1 := User{Username: "alice", Email: "alice@example.com"}
u2 := User{Username: "alice", Email: "alice@example.com"}
u3 := User{Username: "alice", Email: "different@example.com"}
assert.True(t, userEq.Equals(u1, u2)) // Both match
assert.False(t, userEq.Equals(u1, u3)) // Email differs
The Monoid provides an identity element (Empty) that always returns true:
monoid := eq.Monoid[int]() alwaysTrue := monoid.Empty() assert.True(t, alwaysTrue.Equals(1, 2)) // Always true
Curried Equality ¶
The Equals function provides a curried version of equality checking, useful for partial application and functional composition:
intEq := eq.FromStrictEquals[int]()
equals42 := eq.Equals(intEq)(42)
assert.True(t, equals42(42))
assert.False(t, equals42(43))
// Use in higher-order functions
numbers := []int{40, 41, 42, 43, 44}
filtered := array.Filter(equals42)(numbers)
// filtered = [42]
Advanced Examples ¶
Comparing slices element-wise:
sliceEq := eq.FromEquals(func(a, b []int) bool {
if len(a) != len(b) {
return false
}
intEq := eq.FromStrictEquals[int]()
for i := range a {
if !intEq.Equals(a[i], b[i]) {
return false
}
}
return true
})
assert.True(t, sliceEq.Equals([]int{1, 2, 3}, []int{1, 2, 3}))
assert.False(t, sliceEq.Equals([]int{1, 2, 3}, []int{1, 2, 4}))
Comparing maps:
mapEq := eq.FromEquals(func(a, b map[string]int) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if bv, ok := b[k]; !ok || v != bv {
return false
}
}
return true
})
Type Class Laws ¶
Eq instances should satisfy the following laws:
1. Reflexivity: For all x, Equals(x, x) = true 2. Symmetry: For all x, y, Equals(x, y) = Equals(y, x) 3. Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
These laws ensure that Eq behaves as a proper equivalence relation.
Functions ¶
- FromStrictEquals[T comparable]() - Create Eq from Go's == operator
- FromEquals[T any](func(x, y T) bool) - Create Eq from custom comparison
- Empty[T any]() - Create Eq that always returns true
- Equals[T any](Eq[T]) - Curried equality checking
- Contramap[A, B any](func(B) A) - Transform Eq by mapping input type
- Semigroup[A any]() - Combine Eq instances with logical AND
- Monoid[A any]() - Semigroup with identity element
Related Packages ¶
- ord: Provides ordering comparisons (less than, greater than)
- semigroup: Provides the Semigroup abstraction used by Eq
- monoid: Provides the Monoid abstraction used by Eq
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Contramap ¶
Contramap creates an Eq[B] from an Eq[A] by providing a function that maps B to A. This is a contravariant functor operation that allows you to transform equality predicates by mapping the input type. It's particularly useful for comparing complex types by extracting comparable fields.
The name "contramap" comes from category theory, where it represents a contravariant functor. Unlike regular map (covariant), which transforms the output, contramap transforms the input in the opposite direction.
Type Parameters:
- A: The type that has an existing Eq instance
- B: The type for which we want to create an Eq instance
Parameters:
- f: A function that extracts or converts a value of type B to type A
Returns:
- A function that takes an Eq[A] and returns an Eq[B]
The resulting Eq[B] compares two B values by:
- Applying f to both values to get A values
- Using the original Eq[A] to compare those A values
Example - Compare structs by a single field:
type Person struct {
ID int
Name string
Age int
}
// Compare persons by ID only
personEqByID := eq.Contramap(func(p Person) int {
return p.ID
})(eq.FromStrictEquals[int]())
p1 := Person{ID: 1, Name: "Alice", Age: 30}
p2 := Person{ID: 1, Name: "Bob", Age: 25}
assert.True(t, personEqByID.Equals(p1, p2)) // Same ID, different names
Example - Case-insensitive string comparison:
type User struct {
Username string
Email string
}
caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
userEqByUsername := eq.Contramap(func(u User) string {
return u.Username
})(caseInsensitiveEq)
u1 := User{Username: "Alice", Email: "alice@example.com"}
u2 := User{Username: "ALICE", Email: "different@example.com"}
assert.True(t, userEqByUsername.Equals(u1, u2)) // Case-insensitive match
Example - Nested field access:
type Address struct {
City string
}
type Person struct {
Name string
Address Address
}
// Compare persons by city
personEqByCity := eq.Contramap(func(p Person) string {
return p.Address.City
})(eq.FromStrictEquals[string]())
Contramap Law: Contramap must satisfy: Contramap(f)(Contramap(g)(eq)) = Contramap(g ∘ f)(eq) This means contramapping twice is the same as contramapping with the composed function.
func Equals ¶
Equals returns a curried equality checking function. This is useful for partial application and functional composition.
Type Parameters:
- T: The type being compared
Parameters:
- eq: The Eq instance to use for comparison
Returns:
- A function that takes a value and returns another function that checks equality with that value
Example:
intEq := eq.FromStrictEquals[int]()
equals42 := eq.Equals(intEq)(42)
assert.True(t, equals42(42))
assert.False(t, equals42(43))
// Use in higher-order functions
numbers := []int{40, 41, 42, 43, 44}
filtered := array.Filter(equals42)(numbers)
// filtered = [42]
// Partial application
equalsFunc := eq.Equals(intEq)
equals10 := equalsFunc(10)
equals20 := equalsFunc(20)
This is particularly useful when working with functional programming patterns like map, filter, and other higher-order functions.
func Monoid ¶
Monoid returns a Monoid instance for Eq[A]. A Monoid extends Semigroup with an identity element (Empty). For Eq, the identity is an equality predicate that always returns true.
Type Parameters:
- A: The type for which the equality monoid is defined
Returns:
- A Monoid[Eq[A]] with:
- Concat: Combines equality predicates with logical AND (from Semigroup)
- Empty: An equality predicate that always returns true (identity element)
Monoid Laws:
- Left Identity: Concat(Empty(), x) = x
- Right Identity: Concat(x, Empty()) = x
- Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
Example - Using the identity element:
monoid := eq.Monoid[int]() intEq := eq.FromStrictEquals[int]() // Empty is the identity - combining with it doesn't change behavior leftIdentity := monoid.Concat(monoid.Empty(), intEq) rightIdentity := monoid.Concat(intEq, monoid.Empty()) assert.True(t, leftIdentity.Equals(42, 42)) assert.False(t, leftIdentity.Equals(42, 43)) assert.True(t, rightIdentity.Equals(42, 42)) assert.False(t, rightIdentity.Equals(42, 43))
Example - Empty always returns true:
monoid := eq.Monoid[string]()
alwaysTrue := monoid.Empty()
assert.True(t, alwaysTrue.Equals("hello", "world"))
assert.True(t, alwaysTrue.Equals("same", "same"))
assert.True(t, alwaysTrue.Equals("", "anything"))
Example - Building complex equality with fold:
type Person struct {
FirstName string
LastName string
Age int
}
firstNameEq := eq.Contramap(func(p Person) string { return p.FirstName })(eq.FromStrictEquals[string]())
lastNameEq := eq.Contramap(func(p Person) string { return p.LastName })(eq.FromStrictEquals[string]())
ageEq := eq.Contramap(func(p Person) int { return p.Age })(eq.FromStrictEquals[int]())
monoid := eq.Monoid[Person]()
// Combine all predicates - all fields must match
personEq := monoid.Concat(monoid.Concat(firstNameEq, lastNameEq), ageEq)
Use cases:
- Providing a neutral element for equality combinations
- Generic algorithms that require a Monoid instance
- Folding multiple equality predicates into one
- Default "accept everything" equality predicate
func Semigroup ¶
Semigroup returns a Semigroup instance for Eq[A]. A Semigroup provides a way to combine two values of the same type. For Eq, the combination uses logical AND - two values are equal only if they are equal according to BOTH equality predicates.
Type Parameters:
- A: The type for which equality predicates are being combined
Returns:
- A Semigroup[Eq[A]] that combines equality predicates with logical AND
The Concat operation satisfies:
- Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
Example - Combine multiple equality checks:
type User struct {
Username string
Email string
}
usernameEq := eq.Contramap(func(u User) string {
return u.Username
})(eq.FromStrictEquals[string]())
emailEq := eq.Contramap(func(u User) string {
return u.Email
})(eq.FromStrictEquals[string]())
// Users are equal only if BOTH username AND email match
userEq := eq.Semigroup[User]().Concat(usernameEq, emailEq)
u1 := User{Username: "alice", Email: "alice@example.com"}
u2 := User{Username: "alice", Email: "alice@example.com"}
u3 := User{Username: "alice", Email: "different@example.com"}
assert.True(t, userEq.Equals(u1, u2)) // Both match
assert.False(t, userEq.Equals(u1, u3)) // Email differs
Example - Combine multiple field checks:
type Product struct {
ID int
Name string
Price float64
}
idEq := eq.Contramap(func(p Product) int { return p.ID })(eq.FromStrictEquals[int]())
nameEq := eq.Contramap(func(p Product) string { return p.Name })(eq.FromStrictEquals[string]())
priceEq := eq.Contramap(func(p Product) float64 { return p.Price })(eq.FromStrictEquals[float64]())
sg := eq.Semigroup[Product]()
// All three fields must match
productEq := sg.Concat(sg.Concat(idEq, nameEq), priceEq)
Use cases:
- Combining multiple field comparisons for struct equality
- Building complex equality predicates from simpler ones
- Ensuring all conditions are met (logical AND of predicates)
Types ¶
type Eq ¶
type Eq[T any] interface { // Equals returns true if x and y are considered equal according to this equality predicate. // // Parameters: // - x: The first value to compare // - y: The second value to compare // // Returns: // - true if x and y are equal, false otherwise Equals(x, y T) bool }
Eq represents an equality type class for type T. It provides a way to define custom equality semantics for any type, not just those that are comparable with Go's == operator.
Type Parameters:
- T: The type for which equality is defined
Methods:
- Equals(x, y T) bool: Returns true if x and y are considered equal
Laws: An Eq instance must satisfy the equivalence relation laws:
- Reflexivity: Equals(x, x) = true for all x
- Symmetry: Equals(x, y) = Equals(y, x) for all x, y
- Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
Example:
// Create an equality predicate for integers
intEq := eq.FromStrictEquals[int]()
assert.True(t, intEq.Equals(42, 42))
assert.False(t, intEq.Equals(42, 43))
// Create a custom equality predicate
caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
func Empty ¶
Empty returns an Eq instance that always returns true for any comparison. This is the identity element for the Eq Monoid and is useful when you need an equality predicate that accepts everything.
Type Parameters:
- T: The type for which the always-true equality is defined
Returns:
- An Eq[T] where Equals(x, y) always returns true
Example:
alwaysTrue := eq.Empty[int]() assert.True(t, alwaysTrue.Equals(1, 2)) assert.True(t, alwaysTrue.Equals(42, 100)) // Useful as identity in monoid operations monoid := eq.Monoid[string]() combined := monoid.Concat(eq.FromStrictEquals[string](), monoid.Empty()) // combined behaves the same as FromStrictEquals
Use cases:
- As the identity element in Monoid operations
- When you need a placeholder equality that accepts everything
- In generic code that requires an Eq but doesn't need actual comparison
func FromEquals ¶
FromEquals constructs an Eq instance from a custom comparison function. This allows defining equality for any type, including non-comparable types or types that need custom equality semantics.
Type Parameters:
- T: The type for which equality is being defined (can be any type)
Parameters:
- c: A function that takes two values of type T and returns true if they are equal
Returns:
- An Eq[T] that uses the provided function for equality comparison
Example:
// Case-insensitive string equality
caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
// Approximate float equality
approxEq := eq.FromEquals(func(a, b float64) bool {
return math.Abs(a-b) < 0.0001
})
assert.True(t, approxEq.Equals(1.0, 1.00009))
// Custom struct equality (compare by specific fields)
type Person struct { ID int; Name string }
personEq := eq.FromEquals(func(a, b Person) bool {
return a.ID == b.ID // Compare only by ID
})
Note: The provided function should satisfy the equivalence relation laws (reflexivity, symmetry, transitivity) for correct behavior.
func FromStrictEquals ¶
func FromStrictEquals[T comparable]() Eq[T]
FromStrictEquals constructs an Eq instance using Go's built-in == operator. This is the most common way to create an Eq for types that support ==.
Type Parameters:
- T: Must be a comparable type (supports ==)
Returns:
- An Eq[T] that uses == for equality comparison
Example:
intEq := eq.FromStrictEquals[int]()
assert.True(t, intEq.Equals(42, 42))
assert.False(t, intEq.Equals(42, 43))
stringEq := eq.FromStrictEquals[string]()
assert.True(t, stringEq.Equals("hello", "hello"))
assert.False(t, stringEq.Equals("hello", "world"))
Note: For types that are not comparable or require custom equality logic, use FromEquals instead.