Search code examples
concurrencyerlangprologactorerlog

Abstractions for Concurrency in Prolog


We are working on building the 1.0 release of Erlog, a prolog that can run in an Erlang process. For now I have implemented the ability to send and receive Messages via the normal erlang mechanisms. However I would like to be able to add other concurrency abstractions to Erlog that are built on top of erlang's message passing.

So what other concurrency abstractions have people found to be useful in a Prolog or LP programming environment?


Solution

  • tl;dr

    Take note of the main abstractions provided by OTP, deliberately note what situations you recognize as repeating frequently, and develop a standard answer in code that abstracts out whatever ad hoc methods had existed before. (Sounds a lot like refactoring, I suppose.)

    Longer Answer

    So long as processes are cheap enough to spawn that the programmer never considers their cost, and message passing is the only method of sharing data then you've got the high points covered. Based on those humble beginnings, though, a few other patterns usually emerge as obviously useful to have abstractions.

    The first thing you'll probably notice is how often you write ad hoc ways to make synchronous calls safe. As a very shallow example, you'll either do something like (Erlang):

    ask(Proc, Request, Data, Timeout) ->
        Ref = make_ref(),
        Proc ! {self(), Ref, {ask, Request, Data}},
        receive {Ref, Res} -> Res
        after Timeout -> {fail, timeout}
        end.
    

    or

    ask(Proc, Request, Data, Timeout) ->
        Ref = monitor(process, Proc),
        Proc ! {self(), Ref, {ask, Request, Data}},
        receive
            {Ref, Res} ->
                demonitor(Ref, [flush]),
                Res;
            {'DOWN', Ref, process, Proc, Reason} -> {fail, Reason}
        after Timeout -> {fail, timeout}
        end.
    

    or whatever. It doesn't really matter which way you start doing synchronous calls (the unmonitored way or the monitor-demonitor-respond way), the point is you should wrap that pattern up into at least a single function similar to ask/4 above instead of creating ad hoc synchronous calling routines all over the place. (One of my main complaints with raw Erlang projects is that they often identify the need for a sync calling abstraction way too late.)

    This particular issue is at the heart of what gen_server is all about -- handling the common cases that litter raw Erlang code with "seemed like a good idea at the time" inline procedures and abstracting them into a single concept for what service processes generally do.

    Its almost inevitable that you'll wind up developing a general pattern for avoiding deadlocks when you have two identical processes that synchronously signal each other. My typical method for doing this is to spawn an intermediary to deal with it, some systems never let two identically defined processes talk directly (a sort of universal arbitration), sometimes you'll just see "timeout and return to state X" or whatever. Not every way of dealing with potential deadlocks is right for every system, but this is something you will deal with repeatedly as you design concurrent systems. Its not a bad idea to start categorizing the situations where you find deadlocks and abstract away the method(s) you use to avoid them.

    In addition to implementing something like gen_server I'd recommend at least implementing a finite state machine abstraction similar to OTP's gen_fsm. I find this part of OTP even more useful than gen_server in many cases, and after having written a hundred fsm's by hand I really appreciate the utility of having most of this taken care of by gen_fsm (and it still leaves handle_info, which is immensely useful, since its something you would have had to hackishly implement somehow anyway).

    OTP's abstractions generally work at the process level. It may also be helpful to consider what sort of systems you might instantiate which are not single processes. For example, I've been playing with peer-supervised systems a bit, and there are clearly some patterns emerging there. If I pursue this further I will isolate the lessons I'm learning about how those systems work (like the need for a way to triage cancerous processes, etc.) into probably a mix of Erlang behaviors and formal grammars for describing such a system so that I can stop writing process code and instead focus on the level of the problem I am interested in.