acalejos
Hey all!
I found myself needing to do complex conditional logic based on boolean flags. I found myself drawing it up as a truth table and thought it’d be nice to have a way to translate that directly to the logic code, so I made this small 1-file library: Truly
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.
EDIT: Replacing the original examples with these for 0.2.0
Usage
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.
Basic Example
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())
Practical Example
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 Users:
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?")
Modifiers
r- This is effectively like a!function, that will unpack the return tuple and raise on errorsSkip 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).
Example With Modifiers
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.
Here’s some examples of it in action:
https://x.com/ac_alejos/status/1777879977746206816
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
dimitarvp
Thank you for publishing this.
Feedback: make a real-world example because
flag_aandcat1are not telling me anything that I can understand. You can sell it more convincingly that way.acalejos
Hey thanks for the feedback!
This actually spawned from something that came up during my day job so I had to change it to generic terms for the example, but you’re absolutely right that a more practical example would be better.
billylanchantin
Fun little lib! Thanks for publishing. I like the idea of using the markdown truth table as the code.
I’m curious, why
0s and1s instead offalses andtrues? Conserving horizontal space?EDIT: heads up,
TruthTable.evaluate(tt,binding())in the README doesn’t seem real anymore.EDIT 2: Oh looks like
false/trueare supported! Might wanna highlight that as well.linusdm
…or any atom (true and false are atoms too).
acalejos
Good catch on the typo! I had originally called it
TruthTablebut that was actually already taken on Hex.I’ll try to come up with some better examples overall when I get a chance, but yes, it accepts
true, false, 0, 1in the table cells and any existing atoms for column headers and in the final column.al2o3cr
A comment on the implementation: bit-stuffing like in
evaluatehttps://github.com/acalejos/truly/blob/5e42948ed1339b222b2af6b164c0bde10eb52b25/lib/truly.ex#L14-L24
is mandatory in languages where not every type can be a map key.
Elixir isn’t one of those languages, so
actions_mapcould just as well have keys like{true, false, true, false, etc}. There’s likely a memory-vs-performance tradeoff, although I can’t imagine humans maintaining a truth table with more than 8 inputsD4no0
Completely agree! While truth tables and bit shifting are one of the most the most efficient ways to keep data memory-wise, it also imposes a lot of reading overhead from people that are looking to solve a problem.
acalejos
Very fair point!
This is definitely more of a personal preference thing for me, as well as a bit of a vestige of an earlier idea I had to consolidate rows that had the same output.
Like you said though, this will likely not matter in the long run, unless somebody had a heinously large truth table (maybe pulled from a CSV or something?)
Appreciate that you looked through the code!
acalejos
I added another example in an edit to the post. It’s a bit contrived, but hopefully more demonstrative of possible use cases.
acalejos
Releasing a 0.2.0 based on some feedback I received here. Changes include:
1and0are no longer supported, buttrueandfalsestill ares) to toggle strict enforcement of exhaustive enumeration in the table.error, message. This will be return as a tuple of{:error, message}fromTruly.evaluate/2Edited the main post to reflect these changes and added a link to a new video showcasing the changes off.
Thanks for all who’ve commented with feedback!