Reducing degrees of freedom to make rules more dependable.

Run in Livebook

The Problem

Is this user an admin?

defmodule User do

  defstruct [:id, :name, :admin, :owner]

  # Pattern matching
  def admin?(%{admin: true}), do: true
  def admin?(_), do: false

  # Guard
  def is_admin(user) when user.admin == true, do: true

  # Conditional
  def admin_user(user) do
     case user.admin do
        true -> true; 
        _ -> false 
      end
  end
end
user = %{admin: true}

User.admin?(user) # true
User.is_admin(user) # true
User.admin_user(user) # true
!!Map.get(user, :admin) # true

These all answer the question of whether a user is an admin, and they are all syntactically correct. But each carries different assumptions:

  • whether a missing key is false or an error,
  • whether nil is distinct from false,
  • whether “admin” means boolean true or merely truthy.

When solving problems, we tend to focus on the happy path, so it’s the unhappy path where we accidentally introduce semantic assumptions that show up as downstream bugs.

The problem hits LLMs as well; they are good at syntax, but less reliable at choosing between semantically different implementations. This means small changes in prompt wording will produce code with different edge-case behaviour (bugs).

We want to reduce the degrees of freedom. Instead of inventing new shapes for each rule, prefer composing from a smaller set of primitives with known explicit semantics.

Predicate DSL

Elixir has a notion of truthy (!!), where anything except nil and false counts as true. The default for the pred DSL is truthy:

use Funx.Predicate

truthy_name = 
  pred do
    check :name
  end

truthy_name.(%{name: "John"}) # true
truthy_name.(%{name: true}) # true
truthy_name.(%{name: false}) # false
truthy_name.(%{name: nil}) # false
truthy_name.(%{name: ""}) # true

When the key is missing:

truthy_name.(%{test: ""}) # false

Behind the scenes, check :name uses the Prism optic. The projection either finds something (Just the value) or it doesn’t (Nothing). Funx treats Nothing as false, so missing data behaves like a failed check instead of an exception.

If we want to treat missing as an exception, we use the Lens optic:

alias Funx.Optics.Lens

truthy_name = 
  pred do
    check Lens.key(:name)
  end

truthy_name.(%{name: "John"}) # true
truthy_name.(%{name: false}) # false
truthy_name.(%{name: nil}) # false
truthy_name.(%{name: ""}) # true

The lens enforces the key invariant:

truthy_name.(%{test: ""}) # ** (KeyError) key :name not found in:

Here, a missing name key raises immediately (fail fast). This is useful when absence is a bug, not a business rule.

We can also invert a predicate with negate:

falsy_name = 
  pred do
    negate check :name
  end

falsy_name.(%{name: "John"}) # false
falsy_name.(%{name: false}) # true
falsy_name.(%{name: nil}) # true
falsy_name.(%{name: ""}) # false

falsy_name.(%{test: ""}) # true

Often we want an empty string included in our definition of falsy. We can supply this more restrictive logic in a predicate.

required_name = 
  pred do
    check :name, fn value -> !!value and value != "" end
  end

required_name.(%{name: ""}) # false

That works, but Funx includes Required out of the box:

alias Funx.Predicate.Required

required_name = 
  pred do
    check :name, Required
  end

required_name.(%{name: ""}) # false

And if we want the literal boolean true, not truthiness, we can use IsTrue:

alias Funx.Predicate.IsTrue

admin? = 
  pred do
    check :admin, IsTrue
  end

admin?.(%{admin: "Yes"}) # false
admin?.(%{admin: false}) # false
admin?.(%{admin: true}) # true

The goal of the DSL is to make intent clear and easy to read:

admin_or_owner? = 
  pred do
    any do
      check :owner, IsTrue
      check :admin, IsTrue
    end
  end

admin_or_owner?.(%{admin: true}) # true
admin_or_owner?.(%{admin: true, owner: false}) # true
admin_or_owner?.(%{admin: false, owner: false}) # false

Six months from now, we want to be able to read the rule and immediately understand what it does.

The real payoff comes when we model a more complex domain.

Role-playing Game

Let’s revisit our original rules for the role-playing game:

defmodule Status do
  defstruct [:poison, :bleeding, :exposure, :stamina, :blessing, :inventory]
  use Funx.Predicate

  def poisoned? do
    pred do
      check [:poison, :active], fn active -> active == true end
    end
  end

  def bleeding? do
    pred do
      check [:bleeding, :staunched], fn staunched -> staunched == false end
    end
  end

  def poison_resistant? do
    pred do
      check [:blessing, :grants], fn grants -> :poison_resistance in grants end
    end
  end

  def poison_danger? do
    pred do
      poisoned?()
      negate poison_resistant?()
    end
  end

  def severe_bleeding? do
    pred do
      bleeding?()
      check [:bleeding, :severity], fn severity -> severity in [:moderate, :severe, :critical] end
    end
  end

  def wet? do
    pred do
      check [:exposure, :water], fn water -> water in [:wet, :soaked] end
    end
  end

  def charge_building? do
    pred do
      check [:exposure, :electricity], fn electricity -> electricity == :building end
    end
  end

  def electrocution_danger? do
    pred do
      wet?()
      charge_building?()
    end
  end

  def exhausted? do
    pred do
      check :stamina, fn s -> s.current / s.max < 0.25 end
    end
  end

  def collapsed? do
    pred do
      check :stamina, fn s -> s.current / s.max < 0.1 end
    end
  end

  def death_spiral? do
    pred do
      exhausted?()
      bleeding?()
    end
  end

  def mortal_danger? do
    pred do
      any do
        electrocution_danger?()
        death_spiral?()
        severe_bleeding?()
        collapsed?()
      end
    end
  end

  def can_staunch? do
    pred do
      bleeding?()
      check [:inventory, :bandage], fn count -> count > 0 end
    end
  end

  def can_cure_poison? do
    pred do
      poisoned?()
      check [:inventory, :antidote], fn count -> count > 0 end
    end
  end
end

This works, but we’re still hand-coding all the predicates.

Let’s start by extracting that repeated ratio logic using the DSL’s behaviour:

defmodule RatioLessThan do
  @behaviour Funx.Predicate.Dsl.Behaviour

  @impl true
  def pred(opts) do
    threshold = Keyword.fetch!(opts, :value)

    fn %{current: current, max: max} ->
      max != 0 and current / max < threshold
    end
  end
end

And we can use Funx’s built-in predicates for the rest.

defmodule Status do
  defstruct [:poison, :bleeding, :exposure, :stamina, :blessing, :inventory]
  use Funx.Predicate
  alias Funx.Predicate.{Contains, Eq, GreaterThan, In, IsFalse, IsTrue}

  def poisoned? do
    pred do
      check [:poison, :active], IsTrue
    end
  end

  def bleeding? do
    pred do
      check [:bleeding, :staunched], IsFalse
    end
  end

  def poison_resistant? do
    pred do
      check [:blessing, :grants], {Contains, value: :poison_resistance} 
    end
  end

  def poison_danger? do
    pred do
      poisoned?()
      negate poison_resistant?()
    end
  end

  def severe_bleeding? do
    pred do
      bleeding?()
      check [:bleeding, :severity], {In, values: [:moderate, :severe, :critical]} 
    end
  end

  def wet? do
    pred do
      check [:exposure, :water], {In, values: [:wet, :soaked]}
    end
  end

  def charge_building? do
    pred do
      check [:exposure, :electricity], {Eq, value: :building}
    end
  end

  def electrocution_danger? do
    pred do
      wet?()
      charge_building?()
    end
  end

  def exhausted? do
    pred do
      check :stamina, {RatioLessThan, value: 0.25} 
    end
  end

  def collapsed? do
    pred do
      check :stamina, {RatioLessThan, value: 0.1}
    end
  end

  def death_spiral? do
    pred do
      exhausted?()
      bleeding?()
    end
  end

  def mortal_danger? do
    pred do
      any do
        electrocution_danger?()
        death_spiral?()
        severe_bleeding?()
        collapsed?()
      end
    end
  end

  def can_staunch? do
    pred do
      bleeding?()
      check [:inventory, :bandage], {GreaterThan, value: 0}
    end
  end

  def can_cure_poison? do
    pred do
      poisoned?()
      check [:inventory, :antidote], {GreaterThan, value: 0}
    end
  end
end

The predicates now read like their definitions. The anonymous functions are gone, replaced by named parts that carry their semantics.

Note that Eq, In, and Contains all build on Funx’s Eq.Protocol, so they respect custom notions of equality. Also, GreaterThan is order logic, which is built on Funx’s Ord.Protocol.

The Character

A character has a name and a status:

defmodule Character do
  alias Funx.Optics.Lens

  defstruct [:name, :status]

  def status_lens, do: Lens.key(:status)

  def status_check(%__MODULE__{} = character) do
    status = Lens.view!(character, status_lens())

    %{
      poisoned: Status.poisoned?.(status),
      poison_resistant: Status.poison_resistant?.(status),
      poison_danger: Status.poison_danger?.(status),
      bleeding: Status.bleeding?.(status),
      severe_bleeding: Status.severe_bleeding?.(status),
      electrocution_danger: Status.electrocution_danger?.(status),
      exhausted: Status.exhausted?.(status),
      collapsed: Status.collapsed?.(status),
      death_spiral: Status.death_spiral?.(status),
      mortal_danger: Status.mortal_danger?.(status)
    }
  end

  def actions(%__MODULE__{} = character) do
    status = Lens.view!(character, status_lens())

    %{
      can_staunch: Status.can_staunch?.(status),
      can_cure_poison: Status.can_cure_poison?.(status)
    }
  end
end

Here is a character in trouble:

warrior = %Character{
  name: "Wounded Warrior",
  status: %Status{
    poison: %{active: true, source: :spider, severity: :moderate},
    bleeding: %{severity: :light, staunched: false},
    exposure: %{water: :soaked, electricity: :building},
    stamina: %{current: 20, max: 100},
    blessing: %{grants: [:poison_resistance]},
    inventory: %{antidote: 1, bandage: 2}
  }
}

When we apply the status_check/1:

Character.status_check(warrior)

# %{
#   bleeding: true,
#   poisoned: true,
#   poison_resistant: true,
#   poison_danger: false,
#   severe_bleeding: false,
#   electrocution_danger: true,
#   exhausted: true,
#   collapsed: false,
#   death_spiral: true,
#   mortal_danger: true
# }

We find our character is:

  • Poisoned, but resistant: no poison danger
  • Bleeding, but light: no severe bleeding
  • Soaked + charge building: electrocution danger
  • Exhausted + bleeding: death spiral
  • Mortal danger: true
Character.actions(warrior)

# %{can_staunch: true, can_cure_poison: true}

Fortunately, our warrior has some options: they can_staunch and can_cure_poison.

Let’s have them escape the water and apply a bandage:

updated_warrior = %Character{
  name: "Wounded Warrior",
  status: %Status{
    poison: %{active: true, source: :spider, severity: :moderate},
    bleeding: %{severity: :light, staunched: true},
    exposure: %{water: :dry, electricity: :building},
    stamina: %{current: 15, max: 100},
    blessing: %{grants: [:poison_resistance]},
    inventory: %{antidote: 1, bandage: 1}
  }
}

Now when we check their status:

Character.status_check(updated_warrior)

# %{
#   bleeding: false,
#   poisoned: true,
#   poison_resistant: true,
#   poison_danger: false,
#   severe_bleeding: false,
#   electrocution_danger: false,
#   exhausted: true,
#   collapsed: false,
#   death_spiral: false,
#   mortal_danger: false
# }

They are no longer in immediate danger:

  • Electrocution danger: false
  • Death spiral: false
  • Mortal danger: false
Character.actions(updated_warrior)

# %{can_staunch: false, can_cure_poison: true}

Even though they still have a bandage available, they are no longer bleeding, so can_staunch is false. They still have an antidote, so can_cure_poison remains true.

Why It Matters

Reducing degrees of freedom is how we make rules dependable.

The DSL describes intent, not implementation. Every rule follows the same shape, so review becomes about meaning, not edge cases.

The predicates become our ubiquitous language. Names like poisoned?, mortal_danger?, and can_staunch? match how our domain experts talk about the problem. The code becomes the spec.

For LLMs, it is the same advantage. The DSL removes the hardest choice, picking between implementations that look similar but behave differently. Instead, the model selects and composes known parts. Generation becomes assembly, not invention.

Resources

Advanced Functional Programming with Elixir

Dive deeper into functional programming patterns and advanced Elixir techniques. Learn how to build robust, maintainable applications using functional programming principles.

Funx - Functional Programming for Elixir

A library of functional programming abstractions for Elixir, including monads, monoids, Eq, Ord, and more. Built as an ecosystem where learning is the priority from the start.