eq

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 3 Imported by: 0

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
  • 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

func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B]

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:

  1. Applying f to both values to get A values
  2. 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

func Equals[T any](eq Eq[T]) func(T) func(T) bool

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

func Monoid[A any]() M.Monoid[Eq[A]]

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:

  1. Left Identity: Concat(Empty(), x) = x
  2. Right Identity: Concat(x, Empty()) = x
  3. 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

func Semigroup[A any]() S.Semigroup[Eq[A]]

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:

  1. Reflexivity: Equals(x, x) = true for all x
  2. Symmetry: Equals(x, y) = Equals(y, x) for all x, y
  3. 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

func Empty[T any]() Eq[T]

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

func FromEquals[T any](c func(x, y T) bool) Eq[T]

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL