“A complex system that works is invariably found to have evolved from a simple system that worked.” — John Gall

Run in Livebook

What is Free?

Functional programming likes to borrow mathematical terms, one of which is free.

In FP, free means: build a description first, interpret it later.

Here are a couple of basic boolean functions:

positive? = fn x -> x > 0 end
even? = fn x -> rem(x, 2) == 0 end

When we apply them, they collapse to booleans:

positive?.(2)  # true
positive?.(-2) # false
even?.(2)      # true
even?.(1)      # false

We can combine them:

positive_and_even? = fn x -> positive?.(x) and even?.(x) end
positive_or_even?  = fn x -> positive?.(x) or  even?.(x) end

positive_and_even?.(1)  # false
positive_and_even?.(2)  # true
positive_and_even?.(-2) # false

positive_or_even?.(1)   # true
positive_or_even?.(2)   # true
positive_or_even?.(-2)  # true
positive_or_even?.(-1)  # false

We run the checks inline and bake in the grouping (and/or).

Funx has p_all/1 and p_any/1, which take lists of predicates and lets us declare the grouping logic:

positive_and_even? = Funx.Predicate.p_all([positive?, even?])
positive_or_even? = Funx.Predicate.p_any([positive?, even?])

positive_and_even?.(1) # false
positive_or_even?.(1)  # true

Funx also has a predicate DSL, which can be easier to read, particularly for more complex logic.

use Funx.Predicate

positive_and_even? =
  pred do
    positive?
    even?
  end

positive_or_even? =
  pred do
    any do
      positive?
      even?
    end
  end

positive_and_even?.(1) # false
positive_or_even?.(1)  # true

Let’s look at a role-playing game.

The Rules

First, we define our game’s rules:

Predicate Rule
poisoned? Poison is active
poison_resistant? Blessing grants poison resistance
poison_danger? Poisoned AND NOT resistant
bleeding? Bleeding is NOT staunched
severe_bleeding? Bleeding AND moderate+
wet? Water exposure is wet OR soaked
charge_building? Electrical charge building
electrocution_danger? Wet AND charge building
exhausted? Stamina below 25%
collapsed? Stamina below 10%
death_spiral? Exhausted AND bleeding
mortal_danger? Any mortal danger
can_staunch? Bleeding AND has bandage
can_cure_poison? Poisoned AND has antidote

We can express these with predicates:

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

Take a minute to look at this code. These are free predicates. They describe our domain rules without being embedded in control flow, and they are not executed until we interpret them.

When we return in six months, that separation matters. We can quickly see what facts exist, how they build on one another, and where to make a change when the rules evolve, without having to hunt through application logic.

If we have done our job correctly, our subject matter experts should be able to read through these functions and confirm the rules.

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, we are interpreting all of our predicates in two functions, status_check/1 and actions/1.

A character in trouble

Let’s start with a character:

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 in danger:

  • 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:

warrior_after = %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: 20, max: 100},
    blessing: %{grants: [:poison_resistance]},
    inventory: %{antidote: 1, bandage: 1}
  }
}

Now when we check their status:

Character.status_check(warrior_after)

# %{
#   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(warrior_after)

# %{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.

Conclusion

We want our rules to read like rules. We want them named, composed, and grouped in a way we can scan quickly. We want our rules to reflect our domain’s shared language. And when the rules change, we want to be able to quickly dive into the code, make the change, and trust everything built on top will hold.

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.