lib/ord/usage-rules.md

# `Funx.Ord` Usage Rules

## Core Concepts

**Protocol + Custom Ord Pattern**: Use both together for maximum flexibility

- **Protocol implementation** = domain's default ordering (whatever makes business sense)
- **Custom Ord injection** = context-specific ordering when needed
- **Key insight**: Protocol provides sensible defaults, custom Ord provides flexibility

**Contramap**: Contravariant functor - transforms inputs before comparison

- `contramap(&String.length/1, Ord)` compares by string length only
- Mathematical dual of `map` - transforms "backwards" through the data flow  
- Key pattern: transform the input, not the comparison result

**Utils Pattern**: Inject custom Ord logic or default to protocol

- `Ord.Utils.compare(a, b, custom_ord)` - uses custom_ord
- `Ord.Utils.compare(a, b)` - uses protocol dispatch

**Monoid Composition**: Combine ordering logic lexicographically

- `append(ord1, ord2)` - combine two (ord1 then ord2)
- `concat([ord1, ord2, ord3])` - combine list (in sequence)

## Quick Patterns

```elixir
# STEP 1: Implement protocol for domain's default ordering
defimpl Funx.Ord, for: User do
  def lt?(%User{joined_at: a}, %User{joined_at: b}), do: Funx.Ord.lt?(a, b)
  def le?(a, b), do: lt?(a, b) or eq?(a, b)
  def gt?(a, b), do: not le?(a, b)  
  def ge?(a, b), do: not lt?(a, b)
end

# STEP 2: Use protocol directly for default ordering
Ord.lt?(user1, user2)  # Uses protocol (by joined_at)
List.sort(users)       # Uses protocol default

# STEP 3: Inject custom Ord for specific contexts
by_age = Ord.Utils.contramap(& &1.age)
Ord.Utils.compare(user1, user2, by_age)  # Compare by age instead
List.sort(users, by_age)                 # Sort by age, not joined_at

# Combine fields lexicographically
age_then_name = Ord.Utils.concat([
  Ord.Utils.contramap(& &1.age),
  Ord.Utils.contramap(& &1.name)
])

# Use with Funx.List
Funx.List.sort(users, by_age)
Funx.List.strict_sort(users, age_then_name)  # removes duplicates
```

## Key Rules

- **IMPLEMENT PROTOCOL** for domain's default ordering (whatever makes business sense)
- **USE CUSTOM ORD** when you need different ordering for specific operations
- **MUST implement all four** `lt?/2`, `le?/2`, `gt?/2`, `ge?/2` (no optional defaults)
- **Must define total order**: antisymmetric, transitive, connex
- Use `contramap/2` to transform inputs before comparison
- Use monoid functions for composition: `append/2`, `concat/1`  
- Pattern: Protocol for defaults, Utils injection for flexibility
- Keep `Ord` and `Eq` consistent: `compare(a,b) == :eq <=> Eq.eq?(a,b)`

## When to Use

- **Protocol implementation**: When you need domain's default ordering (whatever makes business sense)
- **Custom Ord injection**: When you need different ordering for specific contexts
- Custom sort with `Funx.List.sort/2` (protocol default or custom)
- Range operations (`min`, `max`, `clamp`, `between`)
- Multi-field lexicographic sorting and complex ordering logic

## Anti-Patterns

```elixir
# ❌ Don't use raw operators on structs
if user1 < user2, do: ...  # May raise ArgumentError

# ❌ Don't forget any comparison functions
defimpl Funx.Ord, for: User do
  def lt?(%User{id: id1}, %User{id: id2}), do: id1 < id2
  # Missing le?/2, gt?/2, ge?/2!
end

# ❌ Don't transform comparison result  
contramap(fn result -> not result end)  # Wrong!

# ❌ Don't mix protocols inconsistently
def process(a, b) do
  if a < b do  # Raw operator
    Ord.Utils.max(a, b)  # Protocol-based
  end
end
```

## Testing

```elixir
test "Ord laws hold" do
  # Antisymmetry: a <= b and b <= a implies a == b
  assert Ord.le?(user1, user2) and Ord.le?(user2, user1) 
    implies Ord.Utils.compare(user1, user2) == :eq
  
  # Transitivity: a <= b and b <= c implies a <= c
  assert Ord.le?(user1, user2) and Ord.le?(user2, user3)
    implies Ord.le?(user1, user3)
  
  # Connexity: either a <= b or b <= a
  assert Ord.le?(user1, user2) or Ord.le?(user2, user1)
end

test "contramap preserves Ord laws" do
  by_age = Ord.Utils.contramap(& &1.age)
  user1 = %User{age: 25, name: "Alice"}
  user2 = %User{age: 30, name: "Bob"}
  
  # Contramap projection maintains ordering laws
  assert by_age.lt?.(user1, user2)  # 25 < 30
  assert not by_age.lt?.(user1, user1)  # Anti-reflexive
end

test "monoid composition laws" do
  ord1 = Ord.Utils.contramap(& &1.age)
  ord2 = Ord.Utils.contramap(& &1.name)
  
  # Lexicographic: age first, then name
  combined = Ord.Utils.append(ord1, ord2)
  
  # Same age, different names
  alice = %User{age: 30, name: "Alice"}
  bob = %User{age: 30, name: "Bob"}
  assert combined.lt?.(alice, bob)  # Alice < Bob by name
end
```

## Core Functions

### Protocol Functions

```elixir
# Direct protocol calls
Ord.lt?(a, b)    # less than
Ord.le?(a, b)    # less than or equal  
Ord.gt?(a, b)    # greater than
Ord.ge?(a, b)    # greater than or equal

# These delegate to implementations or fallback to Elixir operators
Ord.lt?(5, 10)              # true (fallback)
Ord.lt?(user1, user2)       # uses User implementation
```

### Utils Functions

```elixir
# Comparison and utilities
Ord.Utils.compare(a, b)           # :lt | :eq | :gt
Ord.Utils.min(a, b)               # minimum value
Ord.Utils.max(a, b)               # maximum value
Ord.Utils.clamp(value, min, max)  # bound value within range
Ord.Utils.between(value, min, max) # check if in range

# For Enum.sort/2 compatibility  
comparator = Ord.Utils.comparator(custom_ord)
Enum.sort(list, comparator)
```

### Transformation Functions

```elixir
# Transform inputs before comparison
by_length = Ord.Utils.contramap(&String.length/1)
Ord.Utils.max("cat", "zebra", by_length)  # "zebra" (longer)

# Reverse ordering
desc = Ord.Utils.reverse()
Ord.Utils.min(3, 7, desc)  # 7 (max in normal order)

# Convert to equality
eq = Ord.Utils.to_eq()
eq.eq?.(5, 5)  # true (compare(5,5) == :eq)
```

### Composition Functions

```elixir
# Combine orderings lexicographically
age_then_name = Ord.Utils.append(
  Ord.Utils.contramap(& &1.age),
  Ord.Utils.contramap(& &1.name)
)

# Combine list of orderings
multi_sort = Ord.Utils.concat([
  Ord.Utils.contramap(& &1.priority),
  Ord.Utils.contramap(& &1.created_at), 
  Ord.Utils.contramap(& &1.id)
])
```

## Integration with Funx.List

```elixir
# Basic sorting
Funx.List.sort([3, 1, 4])  # [1, 3, 4]

# Custom ordering
users = [%User{age: 30}, %User{age: 25}]
by_age = Ord.Utils.contramap(& &1.age)
Funx.List.sort(users, by_age)

# Sort and remove duplicates
Funx.List.strict_sort(users, by_age)  # uses Ord.Utils.to_eq for dedup

# Multi-field sort
by_age_then_name = Ord.Utils.concat([
  Ord.Utils.contramap(& &1.age),
  Ord.Utils.contramap(& &1.name)
])
Funx.List.sort(users, by_age_then_name)
```

## Built-in Implementations

### Temporal Types

```elixir
# DateTime, Date, Time, NaiveDateTime all have safe implementations
events = [%Event{occurred_at: ~U[2024-01-02 10:00:00Z]}, 
          %Event{occurred_at: ~U[2024-01-01 10:00:00Z]}]

by_time = Ord.Utils.contramap(& &1.occurred_at)
Funx.List.sort(events, by_time)  # chronological order
```

### Fallback (Any)

```elixir
# Safe with basic types
Ord.lt?(1, 2)        # true
Ord.lt?("a", "b")    # true  
Ord.lt?([1], [1,2])  # true

# Unsafe with structs/maps - define explicit implementations
# Ord.lt?(%User{}, %User{})  # May raise ArgumentError
```

## Common Patterns

### Multi-field Sorting

```elixir
# Sort by priority (high first), then by created date (old first)
task_ordering = Ord.Utils.concat([
  Ord.Utils.reverse(Ord.Utils.contramap(& &1.priority)),
  Ord.Utils.contramap(& &1.created_at)
])

Funx.List.sort(tasks, task_ordering)
```

### Range Operations

```elixir
# Clamp values within bounds
score = Ord.Utils.clamp(user_score, 0, 100)

# Check if value is in acceptable range  
valid = Ord.Utils.between(temperature, min_temp, max_temp)

# Find extreme values
oldest_user = Enum.reduce(users, &Ord.Utils.min(&1, &2, by_age))
```

### Domain-Specific Ordering

```elixir
defmodule Priority do
  @priorities [:low, :medium, :high, :critical]
  
  def to_index(priority), do: Enum.find_index(@priorities, &(&1 == priority))
end

# Order by priority level
by_priority = Ord.Utils.contramap(&Priority.to_index/1)
Funx.List.sort(tasks, by_priority)
```

## Performance Considerations

- Protocol dispatch has minimal overhead
- `contramap` creates new functions - avoid in tight loops
- Composition with `concat` chains multiple comparisons
- `Funx.List.sort` is optimized for custom comparators
- Built-in temporal comparisons are efficient

## Best Practices

- Define `Ord` for domain types, not just structs
- Keep `Ord` and `Eq` implementations consistent  
- Use `Utils` functions rather than direct protocol calls
- Prefer composition over custom implementations
- Test ordering laws in your implementations
- Document the ordering semantics for domain types