qhwa
Formular - A tiny DSL engine / code evaluator (configuration as code)
Hi, all,
I just published Formular package. It is a tiny library that evaluates a piece of Elixir code.
On the shoulder of Elixir’s Code module
Given a piece of Elixir code (as a string, or AST), Formular runs it with Elixir’s Code module under some security limitations.
So far, the limitations are:
- No calling module functions;
- No calling exit;
- No sending messages.
Indeed, the whole library consists of only one thin module, thanks to the power Elixir has already shipped out of the box.
Motivation
Formular was developed to support some dynamic configuration scenarios. For example, in a scene of an online book store, the discount of a book can be dynamically configured as a piece of code, then evaluated by Formular:
iex> discount_formula = ~s"
...> case order do
...> # old books get a big promotion
...> %{book: %{year: year}} when year < 2000 ->
...> 0.5
...>
...> %{book: %{tags: tags}} ->
...> # Elixir books!
...> if ~s{elixir} in tags do
...> 0.9
...> else
...> 1.0
...> end
...>
...> _ ->
...> 1.0
...> end
...> "
...>
...> book_order = %{
...> book: %{
...> title: "Elixir in Action", year: 2019, tags: ["elixir"]
...> }
...> }
...>
...> Formular.eval(discount_formula, [order: book_order])
{:ok, 0.9}
In such a way, the discount calculation code, which changes frequently, is separated away from the stable business flow, and the primary code is probably more generic and flexible.
I’ve been using it in production for a while so I publish it today in case others may find it useful too.
Cheers!
Most Liked Responses
hauleth
DoS (atom exhaustion):
Formular.eval(~S|for a <- %Range{first: 0, last: 100_000, step: 1}, do: :"#{a}"|, [])
I needed to create range manually, as you do not export ../2 operator.
qhwa
Two new versions have been released:
-
v0.2.2
- brings performance improvements to
0.2.x
- brings performance improvements to
-
v0.3.0
- allows limiting the execution time and heap size
- allows compiling a code string to an Elixir module which brings more performance improvements,
- adds new API
Formula.used_vars/2to extract the used variable names in the formula.
Highlights:
Performance improvements:
A simple benchmark on a simple formula results in this:
Name ips average deviation median 99th %
compiled_module 947.84 K 1.06 μs ±5536.01% 0.83 μs 1.45 μs
eval 14.53 K 68.82 μs ±8.93% 68.69 μs 87.93 μs
eval_ast 4.86 K 205.58 μs ±18.43% 193.89 μs 333.89 μs
Comparison:
compiled_module 947.84 K
eval 14.53 K - 65.23x slower +67.76 μs
eval_ast 4.86 K - 194.85x slower +204.52 μs
The compilation approach is about 80x faster than the original eval approach in v0.2.2, and 300x faster than versions prior to v0.2.2.
Restricted evaluation:
-
limited execution time:
Formular.eval(code, timeout: :timer.seconds(5)) -
limited max heap size by word:
Formular.eval(code, max_heap_size: 15_000)
If limited, the evaluation will be run in a separate process.
Extract used vars API:
This can be helpful if you need to build some UI arround the formula.
iex> Formular.used_vars("a + b - 1")
[:a, :b]








