Erlang Examples: Talk with Erlang

May 30, 2008 - 4 minute read -
erlang exercises

This is part of a series on the Erlang Exercises which is a great set of programming problems that challenge you to implement solutions to some common Erlang problems. I'm going to share some of my solutions to these problems.

Implementing Talk with Distributed Erlang

Make a simple Talk program that makes it possible to chat with friends at other nodes/hosts. Hints: Your program should consist of two registered processes one for reading from the terminal and the other one for recieving messages from the other node then writing them to the terminal.

-module(talk2).
-compile(export_all).
start() ->
    OtherNode = clean(io:get_line('What node? ')),
    FullNode = string:concat(OtherNode, "@localhost"),
    io:format("talking to: ~s~n", [FullNode]),
    register(receiver, spawn(?MODULE, receiver, [])),
    register(sender, spawn(?MODULE, sender, [list_to_atom(FullNode)])),
    get_input().
get_input() ->
    Message = clean(io:get_line('Talk: ')),
    case string:equal("exit!", Message) of
        true ->
            receiver ! done,
            sender ! done;
        false ->
            talk(Message),
            get_input()
    end.
talk(Message) ->
    sender ! {send, Message}.
sender(OtherNode) ->
    receive
        {send, Message} ->
            rpc:call(OtherNode, talk2, send_message, [Message]),
            sender(OtherNode);
        done ->
            void
    end.
send_message(Message) ->
    receiver ! {message, Message}.
receiver() ->
    receive
        {message, Message} ->
            io:format("~s~n", [Message]),
            receiver();
        done ->
            void
    end.
clean(Data) ->
    string:strip(Data, both, $\n).

Explanation

This is functionally similar to Erlang Talk with Sockets. The difference is that it is doing Erlang-to-Erlang only communication taking advantage of Erlang's distributed functionality. The Socket example could have been made to interact with any programming language that can do socket programming.

start() -> This starts the application by asking the user for the name of the other Erlang node to talk to. Erlang nodes are started by running them with a node name erl -name foo. That along with host information are used to talk to a remote node.

So, first using io:get_line('What node? ') we ask the user then name of the other node. Then we spawn 2 processes. One to receive messages and one to send them. The interesting part here is the use of the register function. It takes an atom and a process id. The register function basically binds a pid to an atom and creates a globally accessible pid. It just simplifies the code a bit so you don't have to pass those pids to all of the other functions.

get_input() -> This is the main user input loop. It asks the user for a message and then sends that message to the sender process. io:get_line('Talk: ') waits for user input before it returns, so only after a user types something and hits return will this function continue. If the case string:equal("exit!", Message) do evaluates to true, then the sender and receiver processes are told to exit and the program ends naturally. Otherwise the message is sent.

talk(Message) -> This function sends the message to the sender process.

sender(OtherNode) -> This is the function that is running as the sender process. It is initialized with the remote OtherNode information. It receives messages and forwards them to the OtherNode. The rpc:call(OtherNode, talk2, send_message, [Message]), call invokes the send_message function running on the remote node.

send_message(Message) -> This is really an RPC helper method that passes messages it get to the process running the receive function.

receiver() -> The receiver is the function that is being run as the receiver process. It listens for messages and when it gets one it prints it out to the user.

Thoughts

The other interesting way to solve this would be to actually spawn processes remotely on the other node. The code can then have a reference to a pid running on the remote node and can pass messages to it directly without using the RPC approach. This would allow one side of the conversation to initiate it without the other knowing about it.

register(remote_receiver, spawn(OtherNode, talk2, receiver, [])),
remote_receiver ! "Hello".

An exercise I'll leave up to you!