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