An Erlang Syntactic Sugar Library
August 17, 2017 · View on GitHub
Motivation
I first encountered Erlang around 2014 and fell in love with this "micro operating system" that solved many problems in my work. But when talking with other people, I also heard some complaints about Erlang, most of them about its syntax. One day I discovered an open-source logging library called Lager, which uses the compiler parameter parse_transform to transform source code in customizable ways. So I started learning more about parse_transform and wrote this project. On the one hand, it serves as a demo showing that we can do a lot of cool things with parse_transform; on the other hand, you can also think of it as an Erlang syntactic sugar library that can be used or extended.
Pipe Operator
Many languages (such as F# and Elixir) support a pipeline operator, which passes the result of the previous expression as an argument to the next function call, forming a call chain. For example, if we want to implement an md5 function in Erlang, the traditional way to write it might look like this:
-spec md5(Data :: iodata()) -> Digest :: string().
md5(Data) ->
_Digest = string:to_lower(lists:flatten(lists:map(fun(E) ->
[integer_to_list(X, 16) || X <- [E div 16, E rem 16]]
end, binary_to_list(erlang:md5(Data))))).
If you are tired of the nested calls above, this library allows the following pipeline style, which is equivalent to the traditional notation:
-spec md5(Data :: iodata()) -> Digest :: string().
md5(Data) ->
_Digest = pipe@(Data
! fun erlang:md5/1
! binary_to_list()
! lists:map(fun(E) -> [integer_to_list(X, 16)
|| X <- [E div 16, E rem 16]] end)
! lists:flatten()
! fun string:to_lower/1).
Do Block
One of the most annoying things about writing business logic in Erlang is the rocket-like nesting. For example, let's write a function that returns the product and quotient of two integers.
muldiv(First, Second) ->
case is_integer(First) of
true ->
case is_integer(Second) of
true ->
Product = First * Second,
case Second =/= 0 of
true ->
Quotient = First div Second,
{ok, {Product, Quotient}};
false ->
{error, "Second must not be zero!"}
end;
false ->
{error, "Second must be an integer!"}
end;
false ->
{error, "First must be an integer!"}
end.
Does it look like a rocket sprinting to the right? That is not an exaggeration. Real business logic may contain even more conditional branches, and it becomes even more painful when a change in requirements adds another one. One way to solve this problem is to split the business logic into smaller pieces, but that is not a cure-all you can use every time. Here we provide a Haskell-like do syntax that helps you avoid this annoying nesting.
muldiv(First, Second) ->
do@([esugar_do_transform_error ||
case is_integer(First) of
true -> return(next);
false -> fail("First must be an integer!")
end,
case is_integer(Second) of
true -> return(next);
false -> fail("Second must be an integer!")
end,
Product = First * Second,
case Second =/= 0 of
true -> return(next);
false -> fail("Second must not be zero!")
end,
Quotient = First div Second,
return({Product, Quotient})
]).
To use the do syntax above, you only need to implement your own monad instance (here we implement esugar_do_transform_error).
Import As
Many languages have the concept of modules, and each module contains a large number of functions. When another file wants to use those functions, they need to be imported into that file. Erlang does not usually work that way; instead, it uses the M:F(...) form to call an external function. Of course, it also supports the -import(M, [F/A]). syntax. Once you specify the module name, you can call F(...) directly. We now provide a more powerful import syntax that lets you rename imported functions, so you can call an external function with whatever name you like in the current module.
-module(example).
-compile([{parse_transform, esugar_import_as_transform}]).
-import_as({lists, [{seq/2, range}, {reverse/1, rev}]}).
-export([run/0]).
run() ->
[3, 2, 1] = rev(range(1, 3)).
However, I personally prefer the fully qualified calling style, and I rarely even use the built-in import syntax. This avoids many low-level mistakes, so the import_as syntax implemented here is not something I generally recommend.
Bind Repeatedly
In Erlang, variables are actually immutable. When we bind X to the value 1 with X = 1, we cannot later change it with X = 2. The benefits of immutability are covered in almost every book that introduces functional programming, so I will not say much more here. So how do we "modify" a variable? Erlang's answer is: "You do not modify it. You create a new variable instead. Why do you want to modify it?" So Erlang encourages us to write code like this:
norm(X, Y) ->
T1 = 0,
T2 = T1 + X * X,
T3 = T2 + Y * Y,
T4 = math:sqrt(T3),
T4.
I am exaggerating the example a little here; you certainly do not need to create this many new variables in normal code. However, with repeated binding syntax, we can write it like this:
norm(X, Y) ->
T = 0,
T = T + X * X,
T = T + Y * Y,
T = math:sqrt(T),
T.
Doesn't this contradict Erlang's rule that already-bound variables cannot be modified? Not really. The code above is actually converted into the following intermediate form at compile time:
norm(X@1, Y@1) ->
T@1 = 0,
T@2 = T@1 + X@1 * X@1,
T@3 = T@2 + Y@1 * Y@1,
T@4 = math:sqrt(T@3),
T@4.
That is to say, every time a variable appears on the left side of a new match, even if it has the same name, it is converted into a new numbered variable. When the variable appears on the right side, the latest existing numbered version is used. One question remains: what if I want to use the variable I just bound in the pattern, instead of creating a new one? In that case, you can add @ to the variable.
norm(X, Y) ->
T = 0,
T = T + X * X,
T = T + Y * Y,
T@ = X * X + Y * Y,
T = math:sqrt(T),
T.
It is equivalent to the following:
norm(X@1, Y@1) ->
T@1 = 0,
T@2 = T@1 + X@1 * X@1,
T@3 = T@2 + Y@1 * Y@1,
T@3 = X@1 * X@1 + Y@1 * Y@1,
T@4 = math:sqrt(T@3),
T@4.
At The End
Finally, you may ask: what is the practical use of these features? Honestly, not that much. Nothing becomes impossible without them. As I said at the beginning, you can think of this project as a demo that shows how many interesting things we can do at the syntax level in Erlang. You can also think of it as a library to use or extend, but I cannot guarantee its stability. _(:з」∠)_