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 Libraries Top

deadtrickster
I’ve just released stable versions of my Prometheus Elixir libs: Elixir client [docs]; Ecto collector [docs]; Plugs instrumenter/Export...
New
mcrumm
If you would like to migrate away from node/npm/webpack while still using sass, the dart_sass package provides a installer and runner for...
New
michalmuskala
Another small library today. PersistentEts Hex: persistent_ets | Hex GitHub: GitHub - michalmuskala/persistent_ets Ets table backed by...
New
New
danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have...
New
KallDrexx
For a good number of months I've been working on creating a very basic RTMP live video streaming server. Now that I have a very, very ba...
New
anshuman23
Hello all, I have been working on my proposed project called Tensorflex as part of Google Summer of Code 2018.. Tensorflex can be used f...
New
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Sub Categories:

We're in Beta

About us Mission Statement