View Source Funx.Monoid Usage Rules

LLM Functional Programming Foundation

Key Concepts for LLMs:

CRITICAL Elixir Implementation: All monoid operations are under Funx.Monoid protocol

  • NO separate Semigroup protocol - Elixir protocols cannot be extended after definition
  • Always use Monoid.empty/1, Monoid.append/2 or import Funx.Monoid
  • Different from Haskell's separate Semigroup and Monoid typeclasses

Monoid Protocol: Mathematical structure for associative combination with identity

  • empty/1: Returns the identity element for the monoid (like 0 for addition, [] for lists)
  • append/2: Associative binary operation that combines two values
  • wrap/2 / unwrap/1: Infrastructure functions to convert between raw values and monoid wrappers
  • Example: Monoid.append(%Sum{}, 5, 3) combines numbers using addition

Monoid Laws: Mathematical guarantees that ensure predictable behavior

  • Left Identity: append(empty(m), x) === x
  • Right Identity: append(x, empty(m)) === x
  • Associativity: append(append(a, b), c) === append(a, append(b, c))
  • These laws enable safe composition and parallel computation
  • Example: Summing [1,2,3,4] can be computed as (1+2)+(3+4) or 1+(2+(3+4))

Algebraic Data Combination: Monoids represent ways to combine data

  • Sum: Numbers with addition (0 identity, + operation)
  • Product: Numbers with multiplication (1 identity, * operation)
  • Max/Min: Numbers with comparison (-∞/+∞ identity, max/min operation)
  • List: Collections with concatenation ([] identity, ++ operation)
  • All/Any: Booleans with AND/OR (true/false identity, &&/|| operation)
  • Example: Combining user preferences uses monoid to merge settings

Higher-Level Abstractions: Monoids power utility functions

  • Math.sum/1, Math.product/1 use numeric monoids internally
  • Eq.Utils.concat_all/1 uses All monoid for AND-ing equality checks
  • Predicate.p_all/1 uses All monoid for combining boolean predicates
  • Example: Math.sum([1,2,3]) internally uses Sum monoid but hides the complexity

LLM Decision Guide: When to Use Monoid Protocol

✅ Use Monoid Protocol when:

  • Need associative combination with identity (merging, accumulating, folding)
  • Building reusable combination logic for custom data types
  • Want parallel/distributed computation guarantees
  • Creating utility functions that combine multiple values
  • User says: "combine", "merge", "accumulate", "fold", "reduce"

❌ Don't use Monoid Protocol when:

  • Simple one-off combinations (use direct operations like +, ++)
  • Non-associative operations (like subtraction or division)
  • No meaningful identity element exists
  • Operations have side effects or are non-deterministic

⚡ Monoid Strategy Decision:

  • Built-in types: Use existing monoids (Sum, Product, Max, Min, ListConcat)
  • Custom combination: Define new monoid struct and protocol implementation
  • Application code: Use high-level utilities (Math, Eq.Utils, Ord.Utils)
  • Library code: Expose monoids through utility functions, not raw protocol

⚙️ Function Choice Guide (Mathematical Purpose):

  • Identity element: empty/1 to get neutral value for combination
  • Binary combination: append/2 to combine two values associatively
  • Multiple combination: m_concat/2 to combine a list of values
  • Utility helpers: m_append/3 for low-level monoid operations

LLM Context Clues

User language → Monoid patterns:

  • "combine all these values" → Use m_concat/2 or utility functions
  • "merge with defaults" → Use monoid with appropriate identity element
  • "accumulate results" → Use monoid for associative accumulation
  • "parallel computation" → Monoids enable safe parallelization
  • "sum/product/max/min" → Use Math utilities backed by monoids
  • "AND/OR logic" → Use All/Any monoids for boolean combination

Quick Reference

  • A monoid = empty/1 (identity) + append/2 (associative).
  • Identities must be true identities (e.g. 0 for sum, 1 for product, [] for concatenation).
  • wrap/2 and unwrap/1 exist for infrastructure, not daily use.
  • m_append/3 and m_concat/2 are low-level helpers that power higher abstractions.
  • Application code should prefer helpers in Math, Eq.Utils, Ord.Utils, or Predicate.

Overview

Funx.Monoid defines how values combine under an associative operation with an identity.
Each monoid is represented by a struct (e.g. %Sum{}, %Product{}, %Eq.All{}, %Ord{}) and implements:

  • Monoid.empty/1 → the identity element
  • Monoid.append/2 → associative combination
  • wrap/2 / unwrap/1 → convert between raw values and monoid structs

Important Implementation Detail: Unlike Haskell's separate Semigroup and Monoid typeclasses, Elixir's protocol system limitations require all operations under the single Funx.Monoid protocol.

Monoids are rarely used directly in application code. Instead, they support utilities like Math.sum/1, Eq.Utils.concat_all/1, and Ord.Utils.concat/1.

Protocol Rules

  • Provide all four functions: empty/1, append/2, wrap/2, unwrap/1.
  • Identity: append(empty(m), x) == x == append(x, empty(m)).
  • Associativity: append(append(a, b), c) == append(a, append(b, c)).
  • Purity: results must be deterministic and side-effect free.

Note: While not typically needed, you can define a join/1 operation for monoids that flattens nested monoid values (e.g., combining lists of lists) using m_concat/2. This provides symmetry with Monad operations for flattening nested structures.

Preferred Usage

Go Through Utilities

Use high-level helpers instead of wiring monoids manually:

  • NumbersMath.sum/1, Math.product/1, Math.max/1, Math.min/1
  • EqualityEq.Utils.concat_all/1, Eq.Utils.concat_any/1
  • OrderingOrd.Utils.concat/1, Ord.Utils.append/2
  • PredicatesPredicate.p_and/2, Predicate.p_or/2, Predicate.p_all/1, Predicate.p_any/1

These functions already call m_concat/2 and m_append/3.
You don't need to construct %Monoid.*{} by hand.

Examples

Equality Composition

alias Funx.Eq.Utils, as: EqU

name_eq = EqU.contramap(& &1.name)
age_eq  = EqU.contramap(& &1.age)

EqU.concat_all([name_eq, age_eq])  # AND semantics
EqU.concat_any([name_eq, age_eq])  # OR semantics

Ordering Composition

alias Funx.Ord.Utils, as: OrdU

age  = OrdU.contramap(& &1.age)
name = OrdU.contramap(& &1.name)

OrdU.concat([age, name])  # lexicographic ordering

Math Helpers

alias Funx.Math

Math.sum([1, 2, 3])     # => 6
Math.product([2, 3, 4]) # => 24
Math.max([7, 3, 5])     # => 7
Math.min([7, 3, 5])     # => 3

Interop

  • Eq.Utils relies on Eq.All and Eq.Any monoids for composition.
  • Ord.Utils uses the Ord monoid for lexicographic comparison.
  • Math uses monoids for numeric folds.

Rule of thumb: application code never wires %Monoid.*{} directly—always go through the utility combinators.

Stability Contract

  • Identities must be stable and input-independent.
  • append/2 must be associative for all valid values.
  • wrap/2 and unwrap/1 must be inverses.

Anti-Patterns

  • Hand-wiring %Monoid.*{} in application code.
  • Mixing different monoid types in one append/2.
  • Using fake identities (nil instead of 0 for sum).
  • Hiding side effects inside protocol functions.

Type Safety Warning: Always ensure values passed to append/2 are of the same wrapped monoid type:

# ❌ Wrong - mixing types
append(%Sum{}, 1, %Product{value: 2})  # Invalid - type mismatch

# ✅ Right - consistent types  
append(%Sum{}, 1, 2)                   # OK: both values are integers

Good Patterns

  • Use Math, Eq.Utils, Ord.Utils, or Predicate instead of raw monoids.
  • Keep identities explicit in library code (0, 1, [], Float.min_finite() / Float.max_finite()).
  • Let m_concat/2 and m_append/3 handle the wrapping/combining logic.

When to Define a New Monoid

Define a monoid struct if you need associative combination + identity:

  • Counters, tallies, or scores
  • Config merges (e.g. left-biased / right-biased maps)
  • "Best-of" or "min-by/max-by" selections
  • Predicate or decision combination

Expose it through a utility module—application code should not use it raw.

Built-in Instances

  • %Funx.Monoid.Sum{} — numeric sum (0)
  • %Funx.Monoid.Product{} — numeric product (1)
  • %Funx.Monoid.Max{} — maximum (Float.min_finite())
  • %Funx.Monoid.Min{} — minimum (Float.max_finite())
  • %Funx.Monoid.ListConcat{} — list concatenation ([])
  • %Funx.Monoid.StringConcat{} — string concatenation ("")
  • %Funx.Monoid.Predicate.All{} — predicate AND composition (identity: fn _ -> true end)
  • %Funx.Monoid.Predicate.Any{} — predicate OR composition (identity: fn _ -> false end)
  • %Funx.Monoid.Eq.All{} / %Funx.Monoid.Eq.Any{} — equality composition
  • %Funx.Monoid.Ord{} — ordering composition

These back the higher-level helpers. Use Math, Eq.Utils, Ord.Utils, or Predicate instead.

LLM Code Templates

Basic Monoid Usage Template

defmodule DataAggregator do
  import Funx.Monoid
  alias Funx.Math
  
  # Use high-level utilities instead of raw monoids
  def analyze_numbers(numbers) do
    %{
      sum: Math.sum(numbers),           # Uses Sum monoid internally
      product: Math.product(numbers),   # Uses Product monoid internally  
      maximum: Math.max(numbers),       # Uses Max monoid internally
      minimum: Math.min(numbers)        # Uses Min monoid internally
    }
  end
  
  # Custom combination using monoid utilities
  def combine_stats(stat_list) do
    stat_list
    |> Enum.map(&extract_numbers/1)
    |> Enum.reduce(fn nums1, nums2 ->
      %{
        sum: Math.sum([nums1.sum, nums2.sum]),
        product: Math.product([nums1.product, nums2.product]),
        max: Math.max([nums1.max, nums2.max]),
        min: Math.min([nums1.min, nums2.min])
      }
    end)
  end
end

Custom Monoid Implementation Template

defmodule UserPreferences do
  defstruct theme: :light, notifications: true, language: "en"
end

# Custom monoid for merging user preferences (right-biased)
defmodule Funx.Monoid.UserPreferences do
  defstruct []
  
  defimpl Funx.Monoid do
    def empty(_), do: %UserPreferences{}
    
    def append(_, prefs1, prefs2) do
      # Right-biased merge: prefs2 overwrites prefs1 for non-nil values
      %UserPreferences{
        theme: prefs2.theme || prefs1.theme,
        notifications: if(is_nil(prefs2.notifications), do: prefs1.notifications, else: prefs2.notifications),
        language: prefs2.language || prefs1.language
      }
    end
    
    def wrap(_, prefs), do: prefs
    def unwrap(prefs), do: prefs
  end
end

defmodule PreferencesManager do
  alias Funx.Monoid.Utils, as: MU
  
  def merge_user_preferences(preference_list) do
    # Use monoid to combine multiple preference objects
    MU.m_concat(%Funx.Monoid.UserPreferences{}, preference_list)
  end
  
  def merge_with_defaults(user_prefs, defaults) do
    # Combine with defaults using monoid
    MU.m_append(%Funx.Monoid.UserPreferences{}, defaults, user_prefs)
  end
end

Monoid Law Verification Template

defmodule MonoidLawTester do
  import Funx.Monoid
  
  # Generic test for any monoid implementation
  def verify_monoid_laws(monoid_module, test_values) do
    [a, b, c] = test_values
    m = struct(monoid_module)
    
    # Left Identity: empty + a = a
    left_identity = append(m, empty(m), a) == a
    
    # Right Identity: a + empty = a  
    right_identity = append(m, a, empty(m)) == a
    
    # Associativity: (a + b) + c = a + (b + c)
    left_assoc = append(m, append(m, a, b), c)
    right_assoc = append(m, a, append(m, b, c))
    associativity = left_assoc == right_assoc
    
    %{
      left_identity: left_identity,
      right_identity: right_identity,
      associativity: associativity,
      all_laws_hold: left_identity && right_identity && associativity
    }
  end
  
  # Test built-in monoids
  def test_built_in_monoids() do
    # Test Sum monoid
    sum_result = verify_monoid_laws(Funx.Monoid.Sum, [5, 3, 8])
    IO.inspect(sum_result, label: "Sum monoid laws")
    
    # Test Product monoid  
    product_result = verify_monoid_laws(Funx.Monoid.Product, [2, 3, 4])
    IO.inspect(product_result, label: "Product monoid laws")
    
    # Test List concatenation
    list_result = verify_monoid_laws(Funx.Monoid.ListConcat, [[1, 2], [3], [4, 5]])
    IO.inspect(list_result, label: "ListConcat monoid laws")
  end
end

Parallel Computation with Monoids Template

defmodule ParallelProcessor do
  alias Funx.Math
  
  # Monoids enable safe parallel computation due to associativity
  def parallel_sum(large_list) do
    large_list
    |> Enum.chunk_every(1000)  # Split into chunks
    |> Task.async_stream(&Math.sum/1, max_concurrency: System.schedulers())
    |> Enum.map(fn {:ok, partial_sum} -> partial_sum end)
    |> Math.sum()  # Combine partial results
  end
  
  def parallel_statistics(data_chunks) do
    # Process chunks in parallel, then combine results
    stats = data_chunks
    |> Task.async_stream(fn chunk ->
      %{
        count: length(chunk),
        sum: Math.sum(chunk),
        max: Math.max(chunk),
        min: Math.min(chunk)
      }
    end, max_concurrency: System.schedulers())
    |> Enum.map(fn {:ok, stat} -> stat end)
    
    # Combine partial statistics using monoid properties
    %{
      total_count: Math.sum(Enum.map(stats, & &1.count)),
      total_sum: Math.sum(Enum.map(stats, & &1.sum)),
      overall_max: Math.max(Enum.map(stats, & &1.max)),
      overall_min: Math.min(Enum.map(stats, & &1.min))
    }
  end
end

Utils Integration Template

defmodule MonoidWithUtils do
  alias Funx.Utils
  alias Funx.Math
  
  # Create curried monoid operations
  def build_aggregators() do
    # Curry math operations for reuse
    sum_reducer = Utils.curry_r(&Math.sum/1)
    product_reducer = Utils.curry_r(&Math.product/1)
    max_finder = Utils.curry_r(&Math.max/1)
    
    # Create specialized aggregators
    sum_by = fn key ->
      fn data_list ->
        data_list
        |> Enum.map(&Map.get(&1, key))
        |> Math.sum()
      end
    end
    
    product_by = fn key ->
      fn data_list ->
        data_list
        |> Enum.map(&Map.get(&1, key))  
        |> Math.product()
      end
    end
    
    %{
      sum_reducer: sum_reducer,
      product_reducer: product_reducer,
      max_finder: max_finder,
      sum_by: sum_by,
      product_by: product_by
    }
  end
  
  def analyze_grouped_data(grouped_data) do
    aggregators = build_aggregators()
    
    # Apply different aggregations to different groups
    for {group, data} <- grouped_data do
      {group, %{
        total_score: aggregators.sum_by.(:score).(data),
        multiplied_weights: aggregators.product_by.(:weight).(data),
        count: length(data)
      }}
    end
  end
end

Predicate Integration Template

defmodule MonoidPredicateIntegration do
  alias Funx.Predicate
  alias Funx.Monoid.Utils, as: MU
  alias Funx.Monoid.Predicate.{All, Any}
  
  # Predicates use specific monoids internally for combination
  def build_complex_validators() do
    # Individual predicates  
    is_adult = fn person -> person.age >= 18 end
    has_email = fn person -> String.contains?(person.email, "@") end
    has_name = fn person -> String.length(person.name) > 0 end
    is_verified = fn person -> person.verified end
    
    # Combine using predicate utilities (which use Predicate.All/Any monoids internally)
    # p_all uses m_concat with %All{} monoid
    strict_validator = Predicate.p_all([is_adult, has_email, has_name, is_verified])
    basic_validator = Predicate.p_all([has_email, has_name])
    
    # p_any uses m_concat with %Any{} monoid  
    flexible_validator = Predicate.p_any([is_adult, is_verified])
    
    %{
      strict: strict_validator,
      basic: basic_validator,
      flexible: flexible_validator
    }
  end
  
  # Show how predicates compose via specific monoid types
  def demonstrate_predicate_monoid_connection() do
    # These predicates use Predicate.All/Any monoids internally
    predicate1 = fn x -> x > 0 end
    predicate2 = fn x -> x < 100 end  
    predicate3 = fn x -> rem(x, 2) == 0 end
    
    # p_all uses m_concat(%All{}, predicates) for AND combination
    all_validator = Predicate.p_all([predicate1, predicate2, predicate3])
    
    # p_any uses m_concat(%Any{}, predicates) for OR combination  
    any_validator = Predicate.p_any([predicate1, predicate2, predicate3])
    
    # You could also use monoids directly (though predicates are cleaner)
    manual_all = MU.m_concat(%All{}, [predicate1, predicate2, predicate3])
    manual_any = MU.m_concat(%Any{}, [predicate1, predicate2, predicate3])
    
    # Test values
    test_value = 42
    
    %{
      predicate_all: all_validator.(test_value),  # true (42 > 0 AND 42 < 100 AND even)
      predicate_any: any_validator.(test_value),  # true (42 > 0 OR 42 < 100 OR even)
      manual_all: manual_all.(test_value),        # Same result as predicate_all
      manual_any: manual_any.(test_value)         # Same result as predicate_any
    }
  end
  
  # Show the monoid identities that predicates rely on
  def demonstrate_predicate_monoid_laws() do
    import Funx.Monoid
    
    # All monoid: identity is function that always returns true
    all_identity = empty(%All{})
    IO.inspect(all_identity.(:anything), label: "All monoid identity")  # true
    
    # Any monoid: identity is function that always returns false  
    any_identity = empty(%Any{})
    IO.inspect(any_identity.(:anything), label: "Any monoid identity")  # false
    
    # This is why p_all([]) returns true and p_any([]) returns false
    empty_all = Predicate.p_all([])
    empty_any = Predicate.p_any([])
    
    %{
      empty_all_result: empty_all.(:test),  # true (All identity)
      empty_any_result: empty_any.(:test)   # false (Any identity)
    }
  end
end

LLM Testing Guidance

Test Monoid Laws

defmodule MonoidTest do
  use ExUnit.Case
  import Funx.Monoid
  
  # Test that custom monoids satisfy laws
  test "UserPreferences monoid satisfies laws" do
    prefs1 = %UserPreferences{theme: :dark, language: "en"}
    prefs2 = %UserPreferences{notifications: false}
    prefs3 = %UserPreferences{theme: :light, language: "es"}
    
    monoid = %Funx.Monoid.UserPreferences{}
    
    # Test left identity: empty + a = a
    assert append(monoid, empty(monoid), prefs1) == prefs1
    
    # Test right identity: a + empty = a
    assert append(monoid, prefs1, empty(monoid)) == prefs1
    
    # Test associativity: (a + b) + c = a + (b + c)
    left_assoc = append(monoid, append(monoid, prefs1, prefs2), prefs3)
    right_assoc = append(monoid, prefs1, append(monoid, prefs2, prefs3))
    assert left_assoc == right_assoc
  end
  
  test "Math utilities use monoids correctly" do
    numbers = [1, 2, 3, 4, 5]
    
    # These should be equivalent to manual monoid operations
    assert Math.sum(numbers) == 15
    assert Math.product(numbers) == 120
    
    # Test empty list behavior (should return identity)
    assert Math.sum([]) == 0
    assert Math.product([]) == 1
  end
end

Test Higher-Level Utilities

test "utility functions hide monoid complexity" do
  # Test that utilities work without exposing monoid details
  data = [
    %{score: 10, weight: 0.5},
    %{score: 20, weight: 1.0},  
    %{score: 15, weight: 0.8}
  ]
  
  total_score = data |> Enum.map(& &1.score) |> Math.sum()
  assert total_score == 45
  
  total_weight = data |> Enum.map(& &1.weight) |> Math.sum()
  assert total_weight == 2.3
end

LLM Debugging Tips

Debug Monoid Operations

def debug_monoid_combination(monoid, values) do
  IO.puts("Debugging monoid: #{inspect(monoid)}")
  IO.puts("Identity: #{inspect(empty(monoid))}")
  
  # Show step-by-step combination
  Enum.reduce(values, empty(monoid), fn value, acc ->
    result = append(monoid, acc, value)
    IO.puts("#{inspect(acc)} + #{inspect(value)} = #{inspect(result)}")
    result
  end)
end

# Usage:
# debug_monoid_combination(%Funx.Monoid.Sum{}, [1, 2, 3, 4])

Verify Associativity for Parallel Computing

def verify_parallel_safety(operation, data, chunk_size) do
  # Sequential computation
  sequential_result = operation.(data)
  
  # Parallel computation (different groupings)
  parallel_result1 = data
  |> Enum.chunk_every(chunk_size)
  |> Enum.map(operation)
  |> operation.()
  
  parallel_result2 = data
  |> Enum.chunk_every(chunk_size * 2)  # Different chunk size
  |> Enum.map(operation)
  |> operation.()
  
  %{
    sequential: sequential_result,
    parallel1: parallel_result1,
    parallel2: parallel_result2,
    results_match: sequential_result == parallel_result1 && 
                   parallel_result1 == parallel_result2
  }
end

# Test with Math.sum (which uses Sum monoid)
# verify_parallel_safety(&Math.sum/1, [1,2,3,4,5,6,7,8], 3)

LLM Common Mistakes to Avoid

❌ Don't use raw monoids in application code

# ❌ Wrong: manually constructing monoids
def sum_values(numbers) do
  sum_monoid = %Funx.Monoid.Sum{}
  Enum.reduce(numbers, Monoid.empty(sum_monoid), fn num, acc ->
    Monoid.append(sum_monoid, acc, num)
  end)
end

# ✅ Correct: use utility functions
def sum_values(numbers) do
  Math.sum(numbers)  # Much simpler and clearer
end

❌ Don't ignore monoid laws

# ❌ Wrong: non-associative operation
defmodule BrokenMonoid do
  defstruct []
  
  defimpl Funx.Monoid do
    def empty(_), do: 0
    def append(_, a, b), do: a - b  # Subtraction is NOT associative!
    def wrap(_, x), do: x
    def unwrap(x), do: x
  end
end

# ✅ Correct: ensure associativity
defmodule CorrectMonoid do
  defstruct []
  
  defimpl Funx.Monoid do
    def empty(_), do: 0
    def append(_, a, b), do: a + b  # Addition IS associative
    def wrap(_, x), do: x
    def unwrap(x), do: x
  end
end

❌ Don't use wrong identity elements

# ❌ Wrong: nil is not identity for addition
defmodule BadSumMonoid do  
  defstruct []
  
  defimpl Funx.Monoid do
    def empty(_), do: nil  # Wrong! nil + 5 != 5
    def append(_, a, b), do: (a || 0) + (b || 0)
    def wrap(_, x), do: x  
    def unwrap(x), do: x
  end
end

# ✅ Correct: 0 is the true identity for addition
defmodule GoodSumMonoid do
  defstruct []
  
  defimpl Funx.Monoid do
    def empty(_), do: 0  # Correct! 0 + x = x
    def append(_, a, b), do: a + b
    def wrap(_, x), do: x
    def unwrap(x), do: x
  end
end

Summary

Funx.Monoid provides the mathematical foundation for associative combination with identity. Use it to:

  • Build reusable combination logic: Define monoids for custom data types that need merging
  • Enable parallel computation: Monoid laws guarantee safe parallelization and chunking
  • Power utility functions: Math, Eq.Utils, Ord.Utils, and Predicate all use monoids internally
  • Compose complex operations: Chain monoid operations for sophisticated data processing
  • Ensure mathematical correctness: Monoid laws provide guarantees about behavior

Key Implementation Detail: Unlike Haskell's separate Semigroup and Monoid typeclasses, all operations are under the single Funx.Monoid protocol due to Elixir's protocol limitations.

Best Practice: Use high-level utilities (Math.sum/1, Eq.Utils.concat_all/1) instead of raw monoid operations. Define custom monoids for domain-specific combination needs, but expose them through utility modules rather than direct protocol usage.

Remember: Monoids are about predictable combination. If your operation is associative and has a true identity element, it's probably a monoid and can leverage all the mathematical guarantees and optimizations that come with that structure.