Erlang "ping pong" concurrent programming example translated into SWI Prolog

To teach myself Erlang, I’ve started working through its “getting started” documentation (which is excellent). I generally find the best way to learn a new programming language is to translate examples into a language I’m more comfortable with, so that’s what I’ve done here with the “ping pong” example at https://erlang.org/doc/getting_started/conc_prog.html#message-passing

My translation into SWI Prolog bellow works, but I’m sure more experienced programmers will have tips on how to improve it.

ping(0, _PingQueue, PongQueue) :-
    thread_send_message(PongQueue, 'finished'),
    format("Ping finished~n", []), !.

ping(N, PingQueue, PongQueue) :-
    thread_send_message(PongQueue, msg('Ping', PingQueue)),
    thread_get_message(PingQueue, Msg),
    format("Ping received ~w~n", [Msg]),
    succ(M, N),
    ping(M, PingQueue, PongQueue).

pong(PongQueue) :-
    repeat,
    thread_get_message(PongQueue, Msg),
    (    Msg \== 'finished'
    ->   msg(Ping, PingQueue) = Msg,
         format("Pong received ~w~n", [Ping]),
         thread_send_message(PingQueue, 'Pong'),
         fail
    ;    format("Pong finished~n", []),
         !
    ). 

start :-
    setup_call_cleanup( (message_queue_create(PingQueue), 
                         message_queue_create(PongQueue)
                        ),
                        (thread_create(ping(3, PingQueue, PongQueue), PingThread),
                         thread_create(pong(PongQueue), PongThread)
                        ),
                        (thread_join(PingThread),
                         thread_join(PongThread),
                         message_queue_destroy(PingQueue),
                         message_queue_destroy(PongQueue)
                        )
    ).
1 Like

One way is to start by implementing a few Erlang-ish primitives, like so for example:

:- op(200, xfx, !).

self(Pid) :-
    thread_self(Pid).

spawn(Goal, Pid) :-
    thread_create(Goal, Pid, [detached(true)]).
    
Pid ! Message :-
    catch(thread_send_message(Pid, Message), _, true).
    
receive(Msg) :-
    thread_get_message(Msg).

That gets fairly close, but not quite there.

And then we can do:

ping(0, Pong_Pid) :-
    Pong_Pid ! finished,
    format('Ping finished~n').
ping(N, Pong_Pid) :-
    self(Self),
    Pong_Pid ! ping(Self),
    receive(Msg),
    (   Msg = pong
    ->  format('Ping received pong~n'),
        N1 is N - 1,
        ping(N1, Pong_Pid)
    ;   true
    ).
    
pong :-
    receive(Msg),
    (   Msg = ping(Ping_Pid)
    ->  format('Pong received ping~n'),
        Ping_Pid ! pong,
        pong
    ;   Msg = finished
    ->  format('Pong finished~n')
    ;   true
    ).
   
start :-
    spawn(pong, Pong_Pid),
    spawn(ping(3, Pong_Pid), _).
1 Like

I’m ideologically opposed to syntactic sugar, and Erlang using Pid ! Term as an incomprehensible synonym for send(Pid, Term) reinforced my prejudice. I rank op/3 down with Goto. :laughing:

Prolog would be a much easier to use language if one could dissuade people from creating “write once, read never” line noise with op/3.

Erlang, mercifully, doesn’t seem to have too much illegible ascii art in its syntax besides its use of ! designed to confuse Prolog programmers like me who initiallly assume it means cut.

I would agree if Erlang was not constantly sending stuff. op is extremely valuable to make the common expressions more readable. Your ideology as stated would have us writing

then(then(plus(A,B,C), times(C, D, E)), unify(F, E)).

Or maybe you grant arithmetic and Prolog syntax, but then my CHR program

f(A) \ g(A) <=> h(A) | i(A).

becomes:

<=>(\(f(A),g(A)),'|'(h(A),i(A)))

or more likely:

chr_simpagate([f(A)],[g(A)], h(A),i(A))

No thanks. The use of operators is necessary so we don’t have the basic operations bog down our understanding of the code. You simply need to learn the operators of an example program and then everything becomes easier understanding than not having those operators.

Now, looking at torbjorn’s rewrite, I find the lower code quite confusing if I see it by itself, but if I start by looking at the helper functions, the whole instantly becomes clear.

############

Back to the original topic: Thanks for producing a Prolog version of the Erlang example. I find this educational. It’s fairly basic so I don’t know what could be improved. My one question to Erlangers is whether this example really captures what Erlang can do, because it looks as though Prolog has here exactly the same thing, but Erlang is supposed to be much more capable of parallel processing code.

2 Likes

I’ve actually overcomplicated the original code by creating message queues instead of using the “automagically” created message queue which the PID created by thread_create doubles as. But I find concurrent programming confusing enough without mishmashing threads and message queues.

I’m now busy with the https://erlang.org/doc/getting_started/conc_prog.html#registered-process-names section which shows how to eliminate arguments such as Ping_PID etc by replacing them with global aliases (which is also allowed by SWI Prolog), moving me on to something I’m even more ideogically opposed to than polluting beautifully coherent syntax with infix binary operators.

If i understand you right, you created an extra message queue which neither Prolog nor Erlang requires, so this is not yet a distinction between the languages.

(I don’t much care for global named objects either, so we can agree on that; although if the scope of global is a very small program contained within a single thread, that might not be so big a deal.)

Follows a Logtalk version:

:- object(ping_pong).

	:- threaded.

	:- public(play/1).
	play(Moves) :-
		threaded((
			ping(Moves),
			pong
		)).

	ping(Moves) :-
		(	Moves =:= 0 ->
			write('Game over!\n')
		;	write('Ping ...\n'),
			threaded_notify(throw(Moves)),
			threaded_wait(catch),
			ping(Moves - 1)
		).

	pong :-
		threaded_wait(throw(Moves)),
		write('.... Pong\n'),
		threaded_notify(catch),
		(	Moves =:= 1 ->
			true
		;	pong
		).

:- end_object.

Sample call:

?- ping_pong::play(5).

Ping ...
.... Pong
Ping ...
.... Pong
Ping ...
.... Pong
Ping ...
.... Pong
Ping ...
.... Pong
Game over!
true.

P.S. Example now available at https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/threads/ping_pong

1 Like

Here’s how the pingpong program is written in Erlang:

-module(tut15).
-export([start/0, ping/2, pong/0]).

ping(0, Pong_PID) ->
    Pong_PID ! finished,
    io:format("ping finished");
ping(N, Pong_PID) ->
    Pong_PID ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong")
    end,
    ping(N - 1, Pong_PID).

pong() ->
    receive
        finished ->
            io:format("Pong finished");
        {ping, Ping_PID} ->
            io:format("Pong received ping"),
            Ping_PID ! pong,
            pong()
    end.

start() ->
    Pong_PID = spawn(tut15, pong, []),
    spawn(tut15, ping, [3, Pong_PID]).

Here’s how it looks in Web Prolog …

ping(0, Pong_Pid) :-
    Pong_Pid ! finished,
    io:format('Ping finished').
ping(N, Pong_Pid) :-
    self(Self),
    Pong_Pid ! ping(Self),
    receive({
        pong ->
            io:format('Ping received pong')
    }),
    N1 is N - 1,
    ping(N1, Pong_Pid).
    
pong :-
    receive({
        finished ->
            io:format('Pong finished');
        ping(Ping_Pid) ->
            io:format('Pong received ping'),
            Ping_Pid ! pong,
            pong
    }).

… except that when we start the game, we pass the node option so that the “pinger” and the “ponger” are running on different nodes and thus are playing pingpong over the network (which is something you can easily do in Erlang too):

start :-
    spawn(pong, Pong_Pid, [
        src_predicates([pong/0])
    ]),
    spawn(ping(3, Pong_Pid), _, [
        node('localhost:3020'),
        src_predicates([ping/2])
    ]).

And hey, why shouldn’t distributive programming be this easy!

With Web Prolog, I have always made it a priority to stay as close to Erlang as possible, and that’s how far I got.

As you can see, the receive is different and more high-level than what I implemented above by just calling thread_get_message/1, and there’s more to it than what’s shown here.

2 Likes

Here’s the code with the thread PIDs doubling as message queue PIDs to save a few lines and arguments:

ping(0, Pong_PID) :-
    thread_send_message(Pong_PID, 'finished'),
    format("Ping finished~n", []), !.

ping(N, Pong_PID) :-
    thread_self(Ping_PID),
    thread_send_message(Pong_PID, msg('Ping', Ping_PID)),
    thread_get_message(Ping_PID, Msg),
    format("Ping received ~w~n", [Msg]),
    succ(M, N),
    ping(M, Pong_PID).

pong :-
    repeat,
    thread_get_message(Msg),
    (    Msg \== 'finished'
    ->   msg(Ping, Ping_PID) = Msg,
         format("Pong received ~w~n", [Ping]),
         thread_send_message(Ping_PID, 'Pong'),
         fail
    ;    format("Pong finished~n", []),
         !
    ). 

start :-
    thread_create(pong, Pong_PID),
    thread_create(ping(3, Pong_PID), Ping_PID),
    thread_join(Pong_PID),
    thread_join(Ping_PID).

A reason I prefer the original, more verbose version comes from watching some Youtube lectures given by Joe Armstrong, Alan Kay, Carl Hewitt etc who stressed that message queues were originally a core part of object oriented programming as well as parallel programming – a key idea which somehow got lost in modern, popular programming.

I also prefer placing anything resembling a stream within setup_call_cleanup/3 which this more terse version doesn’t lend itself to.

Using repeat/0 with fail/0 is purely optional, and pong could be written without it like so:

ping(0, Pong_PID) :-
    thread_send_message(Pong_PID, 'finished'),
    format("Ping finished~n", []), !.

ping(N, Pong_PID) :-
    thread_self(Ping_PID),
    thread_send_message(Pong_PID, msg('Ping', Ping_PID)),
    thread_get_message(Ping_PID, Msg),
    format("Ping received ~w~n", [Msg]),
    succ(M, N),
    ping(M, Pong_PID).

pong :-
    thread_get_message(Msg),
    (    Msg \== 'finished'
    ->   msg(Ping, Ping_PID) = Msg,
         format("Pong received ~w~n", [Ping]),
         thread_send_message(Ping_PID, 'Pong'),
         pong
    ;    format("Pong finished~n", [])
    ). 

start :-
    thread_create(pong, Pong_PID),
    thread_create(ping(3, Pong_PID), Ping_PID),
    thread_join(Pong_PID),
    thread_join(Ping_PID).

I explained why I prefer to write listening loops as failure driven loops rather than recursively previously at Some notes on writing a concurrent programing Howto. Comments and corrections welcome

Here is a version of the ping pong example from https://erlang.org/doc/getting_started/robustness.html#time-outs which uses the timeout option in thread_get_message (+Queue, ?Term, +Options) instead of sending a ‘finished’ atom to terminate the thread as above. I changed the timeout to 1 second from the Erlang tutorial’s 5000 miliseconds since I’m impatient.

ping(0, _Pong_PID) :-
    format("Ping finished~n", []), !.

ping(N, Pong_PID) :-
    thread_self(Ping_PID),
    thread_send_message(Pong_PID, msg('Ping', Ping_PID)),
    thread_get_message(Ping_PID, Msg),
    format("Ping received ~w~n", [Msg]),
    succ(M, N),
    ping(M, Pong_PID).

pong :-
    thread_self(Pong_PID),
    repeat,
    (    thread_get_message(Pong_PID, msg(Ping, Ping_PID), [timeout(1)])
    ->   format("Pong received ~w~n", [Ping]),
         thread_send_message(Ping_PID, 'Pong'),
         fail
    ;    format("Pong finished~n", []), !
    ). 

start :-
    thread_create(pong, Pong_PID),
    thread_create(ping(3, Pong_PID), Ping_PID),
    thread_join(Pong_PID),
    thread_join(Ping_PID).