Those of you playing along at home may have gotten the impression that I kinda like this whole Erlang thing. Correct. Those of you playing along at home (and wayward surfers as well) from outside of Europe may be wondering why there's a bothersome 'u' lurking in the spelling of 'behaviour'. Erlang came from Ericsson (who denies the connection between 'erlang' and 'ERicsson LANGuage'. They claim it was named after a mathematician of the same name.) and Ericsson just so happens to reside in Scandanavia.
Linguists theorize that they are allowed to put the letter 'u' wherever they please as a trade-off for having to use that weird o-with-a-line-through-it letter. Or maybe they just want to show the word how much they love empty sets. Regardless, that being said let's hope into gen_server.
gen_server is a component of OTP, the Open Telecom Platform. OTP can be thought of as a sort of application framework for Erlang, much as Python has Pylons, Ruby has Rails, and Windows has Problems Crashing. OTP is hilariously powerful, and being the relative new-comer that I am, I've only just started to really grasp just how powerful it is. To quote Joe Armstrong, author of the fantastic book "Programming Erlang":
The power of OTP comes from the fact that properties such as fault tolerance, scalability, dynamic-code upgrade, and so on, can be provided by the behavior itself. In other words, the writer of the callback does not have to worry about [those things] because this is provided by the behavior.
Okay Philip, what's a behaviour? A behaviour is just what it sounds like - a bucket containing all the common behaviors of a certain kind of system or platform.
In the great tradition of contrived examples, imagine a movie store that would only let you check out one movie at a time. The code for that might look something like what I've included below. Just like last time, first the code and then the explanation. My eternal thanks go out to Jan Lehnardt of the CouchDB project for his assistance, patience, and friendship. In the near future, he's going to be posting a plok entry on this same subject, so by all means go see if that's there. And give him money.
-module(movie_store).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([checkout/2, lookup/1, start_link/0]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).
lookup(Customer) -> gen_server:call(?MODULE, {lookup, Customer}).
init([]) ->
Tab = ets:new(?MODULE, []),
{ok, Tab}.
handle_call({checkout, Customer, Movie}, _From, Tab) ->
Response = case ets:lookup(Tab, Customer) of
[] ->
ets:insert(Tab, {Customer, Movie}),
{ok, Movie};
[{Customer, OtherMovie}] ->
{already_checked_out, OtherMovie}
end,
{reply, Response, Tab};
handle_call({lookup, Customer}, _From, Tab) ->
Reply = case ets:lookup(Tab, Customer) of
[{Customer, Movie}] ->
Movie;
[] ->
none
end,
{reply, Reply, Tab}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Msg, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVersion, State, _Extra) -> {ok, State}.
Okay - it looks terrifying (which incidentally, is Erlang's super power) but it's not. The first line declares the module name (and 'movie_store.erl' should also be the filename) and is a standard part of every Erlang program.
-behaviour(gen_server).can be thought of a line that declares which 'template' we'll be using for this program. The gen_server behaviour mandates that we define the functions specified in the first set of exports.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). are the functions that gen_server expects to find (and will loudly complain if they are absent). I'll delve into more detail on these in the next tutorial, as this is going to be a huge entry as it is. For now, know that all of these have to be defined, even if they do nothing. As you can see at the bottom of the code, handle_cast/2, handle_info/2, terminate/2, and code_change/3 all do precisely nothing.
-export([checkout/2, lookup/1,start_link/0]). makes the custom functions that the server uses available to the outside world. They are in a separate -export statement only for clarity and can be combined with the one above.
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
The starting point for the program. This function as you can see, serves as a wrapper for the command that spins up the server. {local, ?MODULE} shows that the server will be run locally and will not be available to other Erlang nodes in the cluster (as opposed to global). ?MODULE is a stand-in for the name for the current program name and specifies what we'll use to reference the server. The second ?MODULE tells the server where to find the callbacks (in this case, the same file). The remaining options allow you to turn debugging on, log to a file, and a few other things not used in this example.
checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).
lookup(Customer) -> gen_server:call(?MODULE, {lookup, Customer}).
These lines tie in the custom functions and tell gen_server what to do when (in this case) movie_store:checkout(...) and movie_store:lookup(....) are called. ?MODULE indicates where the function is, and {some_name,Arg1, Arg2,...} gives the function name and the arguments to be passed.
init([]) ->
Tab = ets:new(?MODULE, []),
{ok, Tab}.
creates an database table using ets, the built-in memory database and returns it. In a deployed program, this would probably be something a bit more...permanent. A key component of a gen_server program is the program state. In this example, you'll see Tab getting thrown around into everything as it contains the present state of the movie store (who has which movie checked out).
handle_call({checkout, Customer, Movie}, _From, Tab) ->
Response = case ets:lookup(Tab, Customer) of
[] ->
ets:insert(Tab, {Customer, Movie}),
{ok, Movie};
[{Customer, OtherMovie}] ->
{already_checked_out, OtherMovie}
end,
{reply, Response, Tab};
Remember the gen_server:call(....) statements a little bit ago? This is what gen_server calls. You might wonder how there can be two handle_call functions defined with the same arity (the number of arguments, represented by the /x as in "functioname/2"). Look at the first argument, the three element tuple. Now look again at
checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).
The first elements match, which is how gen_server knows which handle_call to call to handle...the..call. Waay too many 'handle's and 'call's in that last bit. The important thing is that, like a lot of other things in Erlang, is based on pattern matching.
Notice that the table, Tab, has also been passed in. We go right into a case/of block that checks the table for the customer to see if they already have a movie checked out. If nothing is found ([] ->...) the movie is checked out to them and {ok, moviename} is returned by the block. If they already have a movie checked out, {already_checked_out, moviename} is returned by the block and they are not allowed to check out another movie (note: for this example, there is no way for a customer to return a movie. That is left as an exercise for the reader.).
In either case, the block's return value is stuffed into a tuple and shot back at whoever called the function. Before moving on to the next handle_call(...), notice the last line: {reply, Response, Tab};. You'd probably expect a period and not a semi-colon. The clauses of the handle_call(...) block are semi-colon delimited and will not work if they are terminated like other functions. I found that one out the hard way.
handle_call({lookup, Customer}, _From, Tab) ->
Reply = case ets:lookup(Tab, Customer) of
[{Customer, Movie}] ->
Movie;
[] ->
none
end,
{reply, Reply, Tab}.
This handle_call/3 defines a function to check and see if someone has a movie checked out. If so, it returns a tuple containing the customer's name and the movie title. Otherwise, the word 'none' is returned. I'm not going to spend a lot of time on this since it's so similar to the handle_call/3 we just looked at.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Msg, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVersion, State, _Extra) -> {ok, State}.
The block of functions that need to be there for gen_server to work, but we're not using this time around. I'll cover these in a later tutorial. A quick note though - code_change/3 is really, really cool. It allows the server's code to be hot-swapped while the server is running. After all, why bother bringing a server down just because the code is changing, right?
Finally, here's how to run it:
2> c(movie_store).
{ok,movie_store}
3> movie_store:start_link().
{ok,<0.42.0>}
4> movie_store:checkout(phil, "Sneakers").
{ok,"Sneakers"}
5> movie_store:lookup(phil).
"Sneakers"
6> movie_store:checkout(phil, "Koyaanisqatsi").
{already_checked_out, "Sneakers"}
7> movie_store:lookup(phil).
"Sneakers"
One problem is that the program doesn't compliment you on your choice of movie if you try to check out 'Koyaanisqatsi'. Well, I've been long-winded enough for one day. I hope this was at the very least, interesting for you. If you improve on this, change it around, or whatever, by all means let me know.