/truly

Create a truth table to consolidate complex boolean conditional logic.

Primary LanguageElixirMIT LicenseMIT

Truly

Truly version Hex Docs Hex Downloads Twitter Follow

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.

Installation

The package can be installed by adding truly to your list of dependencies in mix.exs:

def deps do
  [
    {:truly, "~> 0.2"}
  ]
end

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 error
  • s 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).

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.