Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function chaining in Erlang

Tags:

erlang

It would be nice to create ORM like Active Record or Hibernate, it should process chained queries like this:

User = User:new():for_login(«stackoverflow_admin»):for_password(«1984»):load().

How can we do this? Or just like that, in one line - or at least similar in spirit and meaning.

Maybe there are some preprocessing tools that can help in this?

like image 260
Oleg Chirukhin Avatar asked Jan 05 '16 23:01

Oleg Chirukhin


3 Answers

"Chaining" is a confused form of functional composition, sometimes relying on return values, sometimes operating directly over state mutations. This is not how Erlang works.

Consider:

f(g(H))

is equivalent to:

G = g(H),
f(G)

but may or may not be equivalent to:

g(H).f()

In this form functions are stacked up, and the operations proceed "toward" the return-value side (which is nearly always the left-hand side in most programming languages). In languages where OOP is forced on the programmer as the sole paradigm available (such as Java), however, this function-flowing form is very often not possible without excess noise because of the requirement that all functions (class methods) be essentially namespaced by a class name, whether or not any objects are involved:

F.f(G.g(h.get_h()))

More typically in Java operations over the data are added to the object itself and as much data as possible is held in object instances. Transform methods do not have a "return" value in quite the same way, they instead mutate the internal state, leaving the same object but a "new version" of it. If we think of a mutating method as "returning a new version of the object" then we can think of the dot operator as a bastardized functional composition operator that makes values sort of "flow to the right" as the mutated object is now having additional methods invoked, which may continue to mutate the state as things move along:

query.prepare(some_query).first(100).sort("asc")

In this case the execution "flow" moves to the right, but only because the concept of functional composition has been lost -- we are "ticking" forward along a chain of mutating state events instead of using actual return values. That also means that in these languages we can get some pretty weird flip-flops in the direction of execution if we take stacking too far:

presenter.sort(conn.perform(query.prepare(some_query)).first(100), "asc")

Quick, at a glance tell me what the inner-most value is that kicks that off?

This is not a particularly good idea.

Erlang does not have objects, it has processes, and they don't work the quite way you are imagining above. (This is discussed at length here:Erlang Process vs Java Thread.) Erlang processes cannot call one another or perform operations against one another -- they can only send messages, spawn, monitor and link. That's it. So there is no way for an "implicit return" value (such as in the case of a chain of mutating object values) to have an operation defined over it. There are no implicit returns in Erlang: every operation in Erlang has an explicit return.

In addition to explicit returns and a computational model of isolated-memory concurrent processes instead of shared-memory objects, Erlang code is typically written to "crash fast". Most of the time this means that nearly any function that has a side effect returns a tuple instead of a naked value. Why? Because the assignment operator is also a matching operator, which also makes it an assertion operator. Each line in a section of a program that has side-effects is very often written in a way to assert that an operation was successful or returned an expected type or crash immediately. That way we catch exactly where the failure happened instead of proceeding with possibly faulty or missing data (which happens all the time in OOP chained code -- hence the heavy reliance on exceptions to break out of bad cases).

With your code I'm not sure if the execution (and my eye, as I read it) is supposed to flow from the left to the right, or the other way around:

User = User:new():for_login(«stackoverflow_admin»):for_password(«1984»):load().

An equivalent that is possible in Erlang would be:

User = load(set_password(set_uid(user:new(), "so_admin") "1984"))

But that's just silly. From whence have these mysterious literals arrived? Are we going to call them in-line:

User = load(set_password(set_uid(user:new(), ask_uid()) ask_pw()))

That's going to be pretty awkward to extract yourself from if the user enters an invalid value (like nothing) or disconnects, or times out, etc. It will also be ugly to debug when a corner case is found -- what call to what part failed and how much stuff unrelated to the actual problem is sitting on the stack now waiting for a return value? (Which is where exceptions come in... more on that waking nightmare below.)

Instead the common way to approach this would be something similar to:

register_new_user(Conn) ->
    {ok, Name} = ask_username(Conn),
    {ok, Pass} = ask_password(Conn),
    {ok, User} = create_user(Name, Pass),
    User.

Why would we do this? So that we know when this crashes exactly where it happened -- that will go a long way to telling us how and why it happened. If any of the return values are not a tuple of the shape {ok, Value} the process will crash right there (and most of the time that means taking the connection with it -- which is a good thing). Consider how much exception handling a side-effecty procedure like this actually requires in a language like Java. The long-chain one-liner suddenly becomes a lot more lines:

User =
    try
        User:new():for_login("so_admin"):for_password("1984"):load()
    catch
        {error, {password, Value}} ->
            % Handle it
        {error, {username, Value}} ->
            % Handle it
        {error, db_create} ->
            % Handle it
        {error, dropped_connection} ->
            % Handle it
        {error, timeout} ->
            % Handle it
        %% Any other errors that might possible happen...
    end.

This is one super annoying outcome of uncertain (or even overly long) compositions: it stacks all your error cases together and they have to be handled by propagating exceptions. If you don't throw exceptions in the bad cases above within those calls then you don't know where something went wrong and you have no way of signaling the process or its manager that execution should terminate, retry, etc. The cost of the one line solution is at least an additional dozen lines added only to this one procedure, and we haven't even addressed what should happen in those error handlers!

This is the brilliance of Erlang's "let it crash" philosophy. The code can be written in a way that makes only assumptions of success as long as we assert those assumptions. Error handling code can be extracted out somewhere else (the supervisors) and state can be restored to a known condition outside of the main business logic when something goes wrong. Embracing this creates robust concurrent systems, ignoring it creates brittle crystals.

The cost of all this, however, is those one-line assertions. In concurrent programs this is a profoundly beneficial tradeoff to make.

like image 62
zxq9 Avatar answered Nov 16 '22 15:11

zxq9


Although I found @zxq9 answer so informative, mentioning the history of imitating Java-like OOP style in Erlang could be helpful.

Method chaining needs state and there were efforts to include "Stateful Modules" into Erlang:

  • Parametrized Module: A proposal that lets developer to implement modules that accept parameters and hold them as a module state to use in its functions. Long ago it had been added to Erlang as an experimental feature, but the technical board decided to remove the syntactic support for this feature in R16 because of both conceptual and practical incompatibilities that it caused.

  • Tuple Module: A backwards compatibility for parametrized module, which also introduced in details in chapter 8 of Programming Erlang (Second Edition), but there is no official documentation for it and it is still a controversial feature that introduces complexity and ambiguity with no power in the Erlang way.

Imitating coding style of languages with different paradigms like Ruby and Java in Erlang which is a functional language with different concepts could be exciting but with no added value.

like image 31
Hamidreza Soleimani Avatar answered Nov 16 '22 16:11

Hamidreza Soleimani


Take a look at BossDB. It's a compiler chain and run-time library for accessing a database via Erlang parameterized modules.

like image 1
P_A Avatar answered Nov 16 '22 16:11

P_A