View Source Funx Usage Rules (Index)

Usage rules describe how to use Funx protocols and utilities in practice.
They complement the module docs (which describe what the APIs do).

Each protocol or major module has its own usage-rules.md, stored next to the code.
This index links them together.

Author's Voice and Approach

These usage rules reflect Joseph Koski's approach to functional programming in Elixir, developed alongside his book "Advanced Functional Programming with Elixir".

The documentation emphasizes practical application over academic theory, focusing on real-world patterns, business problems, and incremental adoption. Joseph's philosophy is that functional programming should be approachable and immediately useful, not an abstract mathematical exercise.

When reading these usage rules, you're getting Joseph's perspective on how to effectively apply functional patterns in Elixir production systems.

Available Rules

Conventions

  • Collocation: rules live beside the code they describe.
  • Scope: focus on usage guidance and best practices, not API reference.
  • LLM-friendly: small sections, explicit examples, stable links.

Project Layout (rules only)

lib/
  usage-rules.md            # ← index (this file)
  appendable/
    usage-rules.md          # ← Funx.Appendable rules
  eq/
    usage-rules.md          # ← Funx.Eq rules
  errors/
    validation_error/
      usage-rules.md        # ← Funx.Errors.ValidationError rules
  foldable/
    usage-rules.md          # ← Funx.Foldable rules
  list/
    usage-rules.md          # ← Funx.List rules
  monad/
    usage-rules.md          # ← Funx.Monad rules
    either/
      usage-rules.md        # ← Funx.Monad.Either rules
    effect/
      usage-rules.md        # ← Funx.Monad.Effect rules
    identity/
      usage-rules.md        # ← Funx.Monad.Identity rules
    maybe/
      usage-rules.md        # ← Funx.Monad.Maybe rules
    reader/
      usage-rules.md        # ← Funx.Monad.Reader rules
  monoid/
    usage-rules.md          # ← Funx.Monoid rules
  ord/
    usage-rules.md          # ← Funx.Ord rules
  predicate/
    usage-rules.md          # ← Funx.Predicate rules
  utils/
    usage-rules.md          # ← Funx.Utils rules

Domain Model + Repository Pattern Usage Rules

Core Concepts

Functional Domain-Driven Design: Domain model with validation, healing, and repository patterns using Funx functional programming constructs.

Never-Fail Constructors: Use transformation over validation to create always-valid data structures.

Separate Validation: Domain rules validation is separate from data integrity (healing).

Repository Abstraction: Clean separation between domain logic and storage concerns.

Quick Patterns

# Domain Model Structure
defmodule MyEntity do
  import Funx.Predicate
  alias Funx.Monad.Either
  alias Funx.Errors.ValidationError

  @type t :: %__MODULE__{
    id: pos_integer(),
    required_field: String.t(),
    optional_field: String.t() | nil
  }

  @enforce_keys [:id, :required_field, :optional_field]
  defstruct [:id, :required_field, optional_field: nil]

  # Domain Constants
  @default_value "Default"

  # Predicates (boolean checks)
  def invalid_field?(%__MODULE__{required_field: field}), do: field == @default_value

  # Validation Functions (Either-wrapped)
  def ensure_field(%__MODULE__{} = entity) do
    entity
    |> Either.lift_predicate(
      p_not(&invalid_field?/1),
      fn e -> "Entity '#{e.required_field}' is invalid" end
    )
    |> Either.map_left(&ValidationError.new/1)
  end

  # Complete Validation
  def validate(%__MODULE__{} = entity) do
    entity |> Either.validate([&ensure_field/1])
  end

  # Never-Fail Constructor
  def make(required_field, opts \\ []) do
    %__MODULE__{
      id: :erlang.unique_integer([:positive]),
      required_field: required_field,
      optional_field: Keyword.get(opts, :optional_field)
    }
    |> heal_entity()
  end

  # Safe Change (with healing)
  def change(%__MODULE__{} = entity, attrs) when is_map(attrs) do
    attrs = Map.delete(attrs, :id)
    entity |> struct(attrs) |> heal_entity()
  end

  # Unsafe Change (for testing)
  def unsafe_change(%__MODULE__{} = entity, attrs) when is_map(attrs) do
    attrs = Map.delete(attrs, :id)
    entity |> struct(attrs)
  end

  # Self-Healing Function
  def heal_entity(%__MODULE__{} = entity) do
    %{entity | required_field: heal_field(entity.required_field)}
  end

  defp heal_field(field) when is_binary(field) and byte_size(field) > 0, do: field
  defp heal_field(_), do: @default_value

  # Field Accessors (encapsulation)
  def id(%__MODULE__{id: id}), do: id
  def required_field(%__MODULE__{required_field: field}), do: field
end

# Protocol Implementations
defimpl Funx.Eq, for: MyEntity do
  alias Funx.Eq
  alias MyEntity
  def eq?(%MyEntity{id: v1}, %MyEntity{id: v2}), do: Eq.eq?(v1, v2)
  def not_eq?(%MyEntity{id: v1}, %MyEntity{id: v2}), do: not eq?(v1, v2)
end

defimpl Funx.Ord, for: MyEntity do
  alias Funx.Ord
  alias MyEntity
  def lt?(%MyEntity{required_field: v1}, %MyEntity{required_field: v2}), do: Ord.lt?(v1, v2)
  def le?(%MyEntity{required_field: v1}, %MyEntity{required_field: v2}), do: Ord.le?(v1, v2)
  def gt?(%MyEntity{required_field: v1}, %MyEntity{required_field: v2}), do: Ord.gt?(v1, v2)
  def ge?(%MyEntity{required_field: v1}, %MyEntity{required_field: v2}), do: Ord.ge?(v1, v2)
end

# Repository Pattern
defmodule MyEntity.Repo do
  import Funx.Monad
  import Funx.Utils, only: [curry: 1]

  alias Funx.Monad.Either
  alias Funx.List
  alias MyEntity
  alias Store

  @table_name :my_entity

  def create_table do
    Store.create_table(@table_name)
  end

  def save(%MyEntity{} = entity) do
    insert_entity = curry(&Store.insert_item/2)

    entity
    |> MyEntity.validate()
    |> bind(insert_entity.(@table_name))
  end

  def get(id) when is_integer(id) do
    Store.get_item(@table_name, id)
    |> map(fn data -> struct(MyEntity, data) end)
    |> Either.map_left(fn _ -> :not_found end)
  end

  def list() do
    Store.get_all_items(@table_name)
    |> map(fn items ->
      items
      |> Enum.map(fn item -> struct(MyEntity, item) end)
      |> List.sort()
    end)
    |> Either.get_or_else([])
  end

  def delete(%MyEntity{id: id}) do
    Store.delete_item(@table_name, id)
    |> Either.get_or_else(:ok)
  end
end

Key Rules

Domain Model Rules

  • Always use @enforce_keys for required struct fields
  • Define @type for your struct with proper type annotations
  • Use module constants for domain constraints (@min_value, @default_name, etc.)
  • Predicate functions end with ? and return boolean
  • Validation functions start with ensure_ and return Either
  • Constructor never fails - use make/2 with healing
  • Provide safe/unsafe change - change/2 heals, unsafe_change/2 doesn't
  • Encapsulate with accessors - don't access struct fields directly
  • Use Either.validate/2 to collect all validation errors

Validation Pattern

# 1. Predicate (boolean check)
def invalid_thing?(%__MODULE__{field: value}), do: some_check(value)

# 2. Validation function (Either-wrapped)
def ensure_thing(%__MODULE__{} = entity) do
  entity
  |> Either.lift_predicate(
    p_not(&invalid_thing?/1),
    fn e -> "Descriptive error message with #{e.field}" end
  )
  |> Either.map_left(&ValidationError.new/1)
end

# 3. Add to comprehensive validation
def validate(%__MODULE__{} = entity) do
  entity |> Either.validate([&ensure_thing/1, &ensure_other/1])
end

Protocol Implementation Pattern

# Equality by ID (identity)
defimpl Funx.Eq, for: MyType do
  alias Funx.Eq
  alias MyType
  def eq?(%MyType{id: v1}, %MyType{id: v2}), do: Eq.eq?(v1, v2)
  def not_eq?(%MyType{id: v1}, %MyType{id: v2}), do: not eq?(v1, v2)
end

# Ordering by display field (sorting)
defimpl Funx.Ord, for: MyType do
  alias Funx.Ord
  alias MyType
  def lt?(%MyType{name: v1}, %MyType{name: v2}), do: Ord.lt?(v1, v2)
  def le?(%MyType{name: v1}, %MyType{name: v2}), do: Ord.le?(v1, v2)
  def gt?(%MyType{name: v1}, %MyType{name: v2}), do: Ord.gt?(v1, v2)
  def ge?(%MyType{name: v1}, %MyType{name: v2}), do: Ord.ge?(v1, v2)
end

Repository Rules

  • Import Funx.Monad for bind, map operations
  • Import curry/1 from Funx.Utils for partial application
  • Use @table_name constant for ETS table
  • Always validate before save - validate() |> bind(insert_...)
  • Use curry for partial application - curry(&Store.insert_item/2)
  • Map data back to structs on retrieval
  • Use Either.get_or_else for sensible defaults
  • Handle not_found with Either.map_left
  • Auto-sort lists using protocol-defined ordering

Self-Healing Pattern

def heal_entity(%__MODULE__{} = entity) do
  %{entity |
    field1: heal_field1(entity.field1),
    field2: heal_field2(entity.field2)
  }
end

# Individual field healing functions
defp heal_field1(value) when is_binary(value) and byte_size(value) > 0, do: value
defp heal_field1(_), do: @default_value

defp heal_field2(value) when is_integer(value) and value > 0, do: value
defp heal_field2(_), do: 1

When to Use

  • Domain entities with complex business rules
  • Data that needs validation but should never fail to construct
  • Entities requiring persistence with repository pattern
  • Types needing custom comparison semantics
  • Systems preferring transformation over validation errors

Anti-Patterns

# Don't access struct fields directly
hero.name  # Use Hero.name(hero) instead

# Don't use plain strings in Either.left
Either.left("error")  # Use ValidationError

# Don't mix validation concerns
def make(name) do
  if valid_name?(name) do
    %Hero{name: name}  # This can fail!
  else
    {:error, "invalid"}
  end
end

# Don't forget ID protection in change functions
def change(entity, attrs) do
  struct(entity, attrs)  # Allows ID modification!
end

# Don't skip validation in repository save
def save(entity) do
  Store.insert_item(@table, entity)  # No validation!
end

Testing Patterns

# Use unsafe_change to create invalid entities for testing
invalid_entity = MyEntity.unsafe_change(entity, %{field: "invalid"})

# Test that validation catches issues
case MyEntity.validate(invalid_entity) do
  %Either.Left{left: %ValidationError{errors: errors}} ->
    assert "expected error" in errors
  _ -> flunk("Expected validation failure")
end

# Test self-healing
healed = MyEntity.heal_entity(invalid_entity)
assert MyEntity.field(healed) == "default"

Performance Considerations

  • Self-healing is lightweight transformation
  • Either.validate/2 collects all errors in single pass
  • Protocol dispatch for Eq/Ord is efficient
  • ETS operations are wrapped safely with Either.from_try
  • Currying creates closures - use judiciously

Best Practices

  • Use descriptive error messages with entity context
  • Keep domain constants at module level
  • Implement both Eq and Ord protocols when needed
  • Test both happy path and error accumulation
  • Use repository for all persistence operations
  • Never expose struct fields directly
  • Prefer transformation over validation failure
  • Use curry for reusable partially-applied functions