Create a truth table to consolidate complex boolean conditional logic.
Truly
provides a convenient and human-readable way to store complex conditional logic trees.
You can immediately use Truly.evaluate/2
to evaluate the truth table or pass the truth table
around for repeat use.
You might find this useful for things like feature flags, where depending on the combination of boolean flags you want different behaviors or paths.
This can also make the design much more self-documented, where the intent behind a large logic ladder becomes quite clear.
The package can be installed
by adding truly
to your list of dependencies in mix.exs
:
def deps do
[
{:truly, "~> 0.2"}
]
end
First, you must import Truly
. Then you have the
~TRULY
sigil available.
All column names and result values must be valid (and existing) atoms.
Table cells can only be boolean values.
You must provide an exhaustive truth table, meaning that you provide each combination of column values.
import Truly
columns = [:flag_a, :flag_b, :flag_c]
categories = [:cat1, :cat2, :cat3]
{:ok, tt} = ~TRULY"""
| flag_a | flag_b | flag_c | |
|----------|---------|----------|----------|
| false | false | false | cat1 |
| false | false | true | cat1 |
| false | true | false | cat2 |
| false | true | true | cat1 |
| true | false | false | cat3 |
| true | false | true | cat1 |
| true | true | false | cat2 |
| true | true | true | cat3 |
"""
Truly.evaluate!(tt,[flag_a: true, flag_b: true, flag_c: true])
flag_a = false
flag_b = true
flag_c = false
Truly.evaluate(tt,binding())
Imagine you're writing the backend for your social media app
called PitterPatter
. You want to allow users to direct message
each other, but you want to enforce certain rules around this.
You have the following struct representing your User
:
defmodule User do
defstruct [:dms_open, :locked]
end
You want to control when messages are allowed to be sent according to
the sender's :locked
account status, the receiver's :dms_open
setting,
as well as if the two are friends.
Different combinations of these result in different behavior.
We can define the truth table, and since the result column can be any atom, we can directly pass the function that we want to call:
defmodule PitterPatter do
import Truly
def are_friends(_user1, _user2), do: Enum.random([true,false]) |> IO.inspect(label: "Are friends?")
# We must specify these atoms before the truth table since the atoms must exist already
@flags [:dms_open, :locked]
# Have different functions for different behaviors. You could imagine there
# can be any number of these <= # rows
def send_message(_sender,_receiver,_message), do: "Message Sent!"
def deny_message(_sender,_receiver,_message), do: "Sorry, you can't send that message!"
# Specify our truth table
# For the sake of simplicity we stick to 3 variables
# Also notice that you can use any existing atom (in this case the boolean atoms)
# in the cells
@tt ~TRULY"""
| dms_open | are_friends | locked | |
|----------|--------------|----------|-------------------|
| false | false | false | deny_message |
| false | false | true | deny_message |
| false | true | false | send_message |
| false | true | true | deny_message |
| true | false | false | send_message |
| true | false | true | deny_message |
| true | true | false | send_message |
| true | true | true | deny_message |
"""r # <- Notice the `r` modifier after the table
# This is effectively like a `!` function, that will
# unpack the return tuple and raise on error
def direct_message(sender, receiver, message) do
table = @tt
flags =
[
dms_open: receiver.dms_open,
are_friends: are_friends(sender,receiver),
locked: sender.locked
]
apply(__MODULE__,Truly.evaluate!(table,flags),[sender,receiver,message])
end
end
And just like that, a call to Truly.evaluate!
performs all of the various checks
needed as well as routes to the appropriate function depending on the state passed in.
Let's see how we would use this Let's set up some User
s:
sender = %User{dms_open: true, locked: false}
receiver = %User{dms_open: true, locked: false}
And now you can run PitterPatter.direct_message
, and you will see that
as the :are_friends
status changes (since it's determined randomly above),
the result changes according to the rows in the truth table.
PitterPatter.direct_message(sender, receiver, "Hey, can you talk?")
r
- This is effectively like a!
function, that will unpack the return tuple and raise on errors
Skip validation -- when this modifier is present, we will not check that the truth table is exhaustive (accounts for each possible combination based on present values).
import Truly
columns = [DMS, ARE_FRIENDS, LOCKED]
dms_open_enums = [:friends_only, :public, :closed]
results = [:deny_message, :send_message]
t2 = ~TRULY(
| DMS | ARE_FRIENDS | |
|----------------|--------------|--------------------------------------------------|
| friends_only | false | error, You Must Be Friends to Message This User |
| friends_only | true | send_message |
| public | false | send_message |
| public | true | send_message |
| closed | true | error, This User Does Not Accept Direct Messages |
| closed | false | error, This User Does Not Accept Direct Messages |
)rs
Notice that you can use any existing atom in any cell, and even add error messages in the result column.
If the last row of the previous table were removed, it would result in an error
due to having the s
modifier present.