qhwa

qhwa

Tablex - an implementation of Decision Table in Elixir, for easy-to-read business rules

Hi all!

I’m excited to share with you my recent work on making business rules easier to maintain.

Tablex is an implementation of Decision Table in Elixir. A decision table can make rules more obvious to human brains. Hence we can easily spot the mistakes in rules. Since they are simple markups, Elixir developers can transfer the responsibility of maintaining the domain business rules to operation teammates.

Tablex is heavily inspired by Decision Model and Notation (DMN) and borrowed its ideas of decision tables and friendly expression language. While DMN uses XML to present the decision table, Tablex uses easier-to-read plain texts, such as:

F score   || result
1 >=90    || A
2 60..89  || B
3 -       || C

What is it related to Elixir?

As a library, Tablex has the following abilities:

  • Parsing the above text into a rich Elixir struct, %Tablex.Table{...}.
  • Compiling the above text into corresponding Elixir code that uses pattern matching. (doc)
  • Allowing employing functions in the output definition to be your domain rule engine. (doc)
  • Allowing displaying decision tables in HTML pages with the help of TablexView

In a word, it allows you to work with your business rules conveniently.

Let’s see a more real-life example:

The following Elixir code defines what settings for a given place (identified by city and area id) are:

default_settings = %{
  feature1: [
    param1: 5,
    param2: 0
  ],
  feature2: [
    param1: 0.6,
    param2: 60
  ],
  feature3: [
    feature3_1: [
      param1: 0.75,
      param2: ["SOMETHING"]
    ],
    feature3_2: [
      param1: 0.75,
      param2: ["SOMETHING"]
    ]
  ],
  feature4: false,
  feature5: false,
  feature6: [
    feature6_1: true,
    feature6_2: true,
    feature6_3: false
  ],
  feature7: "provider1",
  feature8: false
}

case target do
  %{city: "Vancouver"} ->
    %{default_settings | feature8: true}

  %{area_id: 2053} ->
    default_settings
    |> put_in([:feature6, :feature6_3], true)
    |> put_in([:feature8], true)
    |> put_in([:feature3, :feature3_1, :param2], ["SOMETHING", "ANOTHER"])
    |> put_in([:feature3, :feature3_2, :param2], ["SOMETHING", "ANOTHER"])

  %{area_id: area_id} when area_id in [7839, 2407, 21094, 62124] ->
    default_settings
    |> put_in([:feature4], true)
    |> put_in([:feature8], true)

  %{area_id: area_id} when area_id in [23982, 2407, 2462, 2179] ->
    %{default_settings | feature7: "provider2"}

  %{area_id: area_id} when area_id in [2060, 2413, 2407, 2417, 2396, 2419] ->
    default_settings
    |> put_in([:feature8], true)
    |> put_in([:feature6, :feature6_3], true)

  _ ->
    default_settings
end

With Decision Table, we can describe almost the same logic in a tabular text block:

====
R                           || 1           2         3                  4                     5                     6
target.city                 || -           Vancouver -                  -                     -
target.area_id              || -           -         2053               7389,2407,21094,62124 23982,2407,2462,2179  2060,2413,2407,2417,2396,2419
====
feature1.param1             || 5           -         -                  -                     -                     -
feature1.param2             || 0           -         -                  -                     -                     -
feature2.param1             || 0.6         -         -                  -                     -                     -
feature2.param2             || 60          -         -                  -                     -                     -
feature3.feature3_1.param1  || 0.75        -         SOMETHING,ANOTHER  -                     -                     -
feature3.feature3_1.param2  || [SOMETHING] -         -                  -                     -                     -
feature3.feature3_2.param1  || 0.75        -         -                  -                     -                     -
feature3.feature3_2.param2  || [SOMETHING] -         SOMETHING,ANOTHER  -                     -                     -
feature4                    || Y           -         -                  Y                     -                     -
feature5                    || N           Y         -                  -                     -                     -
feature6.feature6_1         || Y           -         -                  -                     -                     -
feature6.feature6_2         || Y           -         -                  -                     -                     -
feature6.feature6_3         || N           -         Y                  -                     -                     Y
feature7                    || provider1   -         -                  -                     provider2            -
feature8                    || N           -         Y                  Y                     -                     Y

I said “almost the same” because the settings differ between the Elixir code and the table when the area id is 2407. The maintainer of the code intended to set multiple settings for area #2407 with different clauses of pattern matching. But he didn’t know that only the first matched clause was hit. This mistake was quickly spotted when the rules were present on a table.

Also, here we use R, the reverse merge hit policy so that settings will be merged with all hit rules from right to left. So rules #6, #5, #4, and #1 will all impact the final result.

The textural table is easy to read and can be further visualized with HTML:


(generated by TablexView)

Roadmap

1.0

  • formatter
  • expression specification

1.1

  • more data types (Date, Time, and DateTime)

Current status

It’s still a very early stage for Tablex, although it has been used in production. Currently, the latest version is 0.3.1.

I hope you can find this helpful to you. Feedback is welcome! Thanks!

Most Liked

qhwa

qhwa

New version released today: v0.2.0-alpha.1, with many exciting changes:

  • An optimizer is added for optimizing the rules, for example, to remove duplicated or unreachable rules.
  • A formatter that can format the table text so that the style is persistent.
  • Some APIs for getting or updating rules, so that the rules can be programmably managed without handwriting tables.
  • Add support for structs as decision arguments (PR), thanks to László Hegedüs
  • Improvement on development, such as allowing development on Elixir 1.14, and adding CI, thanks to James Every

I’m very excited about what is ahead. Feedback is very welcome!

qhwa

qhwa

New version released: v0.3.1, with some improvements and bugfixes:

  • [BREAKING] Tablex no longer alters the case of a variable name. For example, myFoo will stay as myFoo. In contrast, previous versions will convert it to my_foo.
  • Performance is significantly improved when generating Elixir code from tables.
qhwa

qhwa

New version released: v0.2.0. Compared to v0.2.0-alpha.1, there are many changes:

  • A better support for Emojis :grinning:
  • Bugfixes for the formatter
  • Bugfixes for the optimizer

Where Next?

Popular in Announcing Top

mischov
import Meeseeks.CSS html = HTTPoison.get!("https://news.ycombinator.com/").body for story <- Meeseeks.all(html, css("tr.athing")) do...
New
jakub-zawislak
Hi everyone, I’m coming from the Symfony (PHP) framework. I like Phoenix, but it has a one thing that was build much better in the Symfo...
New
tmbb
I’ve been working on two packages (not on hex.pm yet) to build admin interfaces for phoenix apps: bureaucrat - which contains a bunch ...
New
oltarasenko
Dear Elixir community, After a year of development, bug fixes, and improvements, we are proudly ready to share the release of Crawly 0.1...
New
nikokozak
Hello all, I’ve been working on Svonix - a library for quickly integrating Svelte components into Phoenix views. It’s a much-needed succ...
New
kip
Image is an image processing library for Elixir. It is based upon the fabulous vix library that provides a libvips wrapper for Elixir. I...
622 18934 194
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12645 134
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New
marcuslankenau
I feel kind of stuck with the absence of a proper xml library for Elixir. Currently I use SweetXML which was ok for me more or less to pa...
New

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement