Going for Erlang here.
At a basic level, two options are available. If you're only wanting to use become
to change the behavior of a given process (see point 2 of the list at section 2.1.3), then it's just a question of calling the next loop with a different recursive function:
loop(State) ->
receive
{normal, Msg} -> loop(State);
{change, NewLoop} -> NewLoop(State)
end.
Assuming NewLoop
is a higher order function, whenever you send the message {change, NewLoop}
to a process initially running the function loop/1
, It will then use NewLoop
as its definition.
The second option is the one where you want the process to act as a proxy (and change behavior). This is similar to what Marcelo Cantos suggested. Just have the process loop and forward messages to a new one (stealing his code):
become(Pid) ->
receive
Msg -> Pid ! Msg
end,
become(Pid).
Theoretically, this does what the paper would ask for. In practice, though, there are risks in using the second option in a real life Erlang system. In communications between two processes, it is a frequent concept to 'stamp' the message with the sender's process identifier and that the reply will be tagged with the receiver's process identifier. An exchange of the following messages could be done (this is not code, just a hand notation):
A = <0.42.0> <-- process identifier
B = <0.54.0>,
A: {A, "hello"},
B: {B, "hi"},
A: {A, "how are you?"}.
B: {B, "Fine!"}.
So when A
expects a message from B
, it will be able to match only for these by using a pattern such as {B, Message}
. In the case of a forwarded message, this addressing scheme becomes invalid and simply broken.
An alternative would be to use references (make_ref()
) as an addressing scheme to match messages on top of the returning Pids. This would separate the use of the Pid as an address and an identifier by using two different entities.
There is another problem, even if the addressing is safe: process dependencies. What happens for named processes, crashing processes, monitors, etc? The client processes might have monitors, links and whatnot all set up to make sure nothing goes wrong without being notified. By modifying the routing process to trap exit signals and forward them, it should be possible to make things safer:
loop(State) ->
receive
{normal, Msg} -> loop(State);
{become, Pid} ->
process_flag(trap_exit, true), % catch exit signals as messages
link(Pid), % make it so if one process crashes, the other receives the signal
become(Pid)
end.
become(Pid) ->
receive
{'EXIT', Pid, killed} -> exit(self(), kill); %% uncatchable termination
{'EXIT', Pid, normal} -> ok; %% die normally too
{'EXIT', Pid, Reason} -> exit(Reason); %% die for the same reason as Pid
Msg -> Pid ! Msg %% forward the message
end,
become(Pid).
This tested code should be safer as the processes depending on the first process will get the same error messages as the one represented by Pid
in become(Pid)
, making the routing rather transparent. However, I wouldn't give any guarantees that this would work in the long run with real life applications.
Even though it is possible and conceptually simple enough to represent and do things like become
, Erlang's standard library was just not really thought with the second use case in mind. For real world applications, I can only recommend the first method, which is used extensively by every Erlang application that exists right now. The second one is uncommon and might cause problems
**The calls to the function become/1
in the last example should likely be ?MODULE:become(Pid)
to avoid potential crashes related to hot code loading in the future. *