Funx: Reducing Degrees of Freedom
Reducing degrees of freedom to make rules more dependable.
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
falseor an error, - whether
nilis distinct fromfalse, - whether “admin” means boolean
trueor 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.