Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Seven Languages In Seven Weeks: Erlang - Day 3

By Ben Nadel on
Tags: Erlang

I just sort of finished day 3 of Erlang from my Seven Languages in Seven Weeks book. I hesitate to say "finished" because I only answered one of the homework problems; but, being that this day was all about concurrency, I actually view the one homework problem as a victory! And, given the fact that the first concurrency problem took me about three hours to solve, I can only imagine that answsering the other 4 problems would have taken a very significant amount of time.

For question number 2, I had to create a process that could restart itself. I put about an hour of research into the problem, but came up with nothing. I found all kinds of interesting information about "supervisors" and child processes and restart strategies; but, I couldn't find anything about a process that could restart itself.

By question number 3, I'll be honest, I was just emotionally drained. Getting concurrency to work felt like such a battle that by the time I gave up on #2, I pretty much gave up on the rest of the homework.

But, I'm not going to be too hard on myself; as much as I hate giving up, I do feel like I did make some progress.

HW1: Monitor the translate_service and restart it should it die.

For this problem, we have a process that handles concurrenct translations requests. But, I didn't want the root application process to have to worry about monitoring the service; as such, I decided to create a third process that would spawn the translation service and then monitor it. Before we look at the code, though, it might be useful to think about this graphically:

Using Concurrent Processes In Erlang.

OK, I'm not sure if you should be more confused or less confused by that graphic. Basically what happens is that we have the root process spawn the Tranlsator process. This Translator process then spawns the translation service (a 3rd process). It does this so that it can then monitor the translation service and restart it if it dies.

When the Translator spawns the translation service, it registers the spawned process as the atom, translation_service (which you'll see in the code). This allows the translation service process to be referenced outside of the spawning context. However, coming from an Object Oriented background, I couldn't help but feel that this was a blantant violation of encapsulation. As such, I restrict the use of the translation_service atom to the "hw1" module. However, in order to make that translation service usable by the root process, I then had to create another function within the hw1 module, translate/1, that could message the translation service using the registered atom.

I think looking at the code will help codify the concepts. Here is the hw1 module:

%-- Monitor the translate_service and restart it should it die.

-module( hw1 ).
-export( [translate/1, wait_for_translate/0, translator/0] ).

%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%

%-- NOTE: The following methods will register and then use the
%-- atom: translation_service. This is done so that spawned threads
%-- can be referenced without a PID (process ID). However, since
%-- it only ever registered / used within this module, I feel that
%-- it is not a horrible break of encapsulation.

%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%

%-- I ask the given word to be translated (by the translation
%-- service) and then wait for a response.
%-- NOTE: This function is meant to be called by the ROOT process
%-- and is here as a convenience.
translate( Word ) ->

	%-- Send a translation request to the *registered* translation
	%-- service process.
	translation_service ! {self(), Word},

	%-- Wait for a translation response to come back.
	%-- NOTE: Since the translation process is known to die, we want
	%-- to create a timeout here so that we don't end up blocking
	%-- the root process (THIS PROCESS) indefinitely.

		%-- When the translation message comes back, return a tuple
		%-- containing the From/To values.
		{translation, From, To} -> {From, To}

	%-- Set up a timeout.

		%-- After 3000 milliseconds, respond.
		3000 ->

			%-- The service timed out.
			"Service is not responding in a timely manner."



%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%

%-- I handle asynchronous requests for translation. I just sit here
%-- and block *my* spawned process, waiting for requests to come in.
%-- NOTE: I am meant to reside in my own process.
wait_for_translate() ->

	%-- I block the spawned process until I get a request that
	%-- matches one of the following case cases.

		%-- Pass message back to caller and return to waiting.
		{From, "casa"} ->
			From ! {translation, "casa", "house"},

		%-- Pass message back to caller and return to waiting.
		{From, "blanca"} ->
			From ! {translation, "casa", "white"},

		%-- For this term, we are going to DIE.
		{From, "blam"} ->
			exit( {hw1, die, at, erlang:time()} );

		%-- Unknown term - pass back error message.
		{From, Word} ->
			From ! {translation, Word, "Woop! Don't know what that means!"},



%-- -------------------------------------------------------- --%
%-- -------------------------------------------------------- --%

%-- I manage the translation service, starting it, and restarting it
%-- if it should ever die.
%-- NOTE: I am meant to reside in my own process.
translator() ->

	%-- I flag that the current process will handle any exceptions
	%-- sent to it by concurrent threads. When this is true,
	%-- exceptions will be uniformly packaged and sent as messages
	%-- that can be properly received.
	process_flag( trap_exit, true ),

	%-- I block *my* spawned process until I get a request that
	%-- matches one of the following case statements.

		%-- I handle mssages for starting a new translation service.
		start_service ->

			%-- Debug information.
			io:format( "Translation server has been started.~n" ),

			%-- Spawn the traslation process and link it to this one.
			%-- In doing so, this process will be able to monitor the
			%-- health of the linked process.
			%-- NOTE: The resitered atom, translation_service, is
			%-- only used within this module.
				spawn_link( fun wait_for_translate/0 )

			%-- Wait for more asynchronous messages.

		%-- I handle any exit messages that have been raised.
		{'EXIT', From, Reason} ->

			%-- Debugging information.
			io:format( "Translation service has died.~n" ),

			%-- Send a message to self to restart the service.
			self() ! start_service,

			%-- Wait for more asynchronous messages.



There are three functions in this module. The latter two will be used to initialize and power concurrent threads (Translator and the translation service). The first function, translate/1, acts as the bridge between the root process and the translation service concurrent process. You'll notice that when the translate/1 function passes an asynchronous message to the translation_service atom, it passes a reference to self(). This provides the translation_service with a PID (process ID) to which the translation response will be sent.

Let's compile it and translate some words. You'll notice in the above code that the request to translate the term, "blam," will kill the translation service (which will then be restarted by the Translator process):

1> c( hw1 ).
./hw1.erl:85: Warning: variable 'From' is unused
./hw1.erl:139: Warning: variable 'From' is unused
./hw1.erl:139: Warning: variable 'Reason' is unused
2> Translator = spawn( fun hw1:translator/0 ).
3> Translator ! start_service.
Translation server has been started.
4> hw1:translate( "casa" ).
5> hw1:translate( "blanca" ).
6> hw1:translate( "foozie" ).
{"foozie","Woop! Don't know what that means!"}
7> hw1:translate( "blam" ).
Translation service has died.
Translation server has been started.
"Service is not responding in a timely manner."
8> hw1:translate( "casa" ).

As you can see, when we tried to trasnlate the phrase, "blam", the translation service exits out and is subsequently restarted by the Translator process. Our traslate/1 function, however, does not know about this restart. The problem with that is that the receive construct within the tranlsate/1 function is going to block the root process, waiting for a synchronous response. In order to make sure that translate/1 doesn't wait indefinitely, we use the "after" clause to setup a timeout. This way, if the translation_service dies, our translate/1 function will quickly timeout and return control back to the root process.

I still feel like concurrency is beyond my undrestanding; but, I do feel like I am making some progress on the topic. I think the most important take away that I got from this homework was the idea that a process is spawned and then initialized with a given function. Once the process is initialized, however, it can then make calls to other functions. As the process is running, it is critical that the process is constantly waiting for new messages (via receive), otherwise, it will not be able to respond.

I feel bad that I couldn't get through this homework; but, I think Clojure (up next) and Haskell will give me more opportunities to get in touch with my concurrent side.

Reader Comments