Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Funx: Free Your Predicates ```elixir Mix.install([ {:funx, "0.8.2"} ]) ``` ## 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: ```elixir positive? = fn x -> x > 0 end even? = fn x -> rem(x, 2) == 0 end ``` When we apply them, they collapse to booleans: ```elixir positive?.(2) # true positive?.(-2) # false even?.(2) # true even?.(1) # false ``` We can combine them: ```elixir 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: ```elixir 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 a bit easier to read, particularly for more complex logic. ```elixir 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: ```elixir 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: ```elixir 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: ```elixir 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`: ```elixir Character.status_check(warrior) ``` 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 ```elixir Character.actions(warrior) ``` Fortunately, our warrior has some options: they `can_staunch` and `can_cure_poison`. Let's have them escape the water and apply a bandage: ```elixir 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: ```elixir Character.status_check(warrior_after) ``` They are no longer in immediate danger: * Electrocution danger: false * Death spiral: false * Mortal danger: false ```elixir Character.actions(warrior_after) ``` 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.
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More