Et kassaapparat i Erlang


søndag 2. mai 2010 Erlang

CropperCapture[67] Den første oppgaven jeg gav lærlingen min var å programmere et enkelt kommandolinje-basert kassaapparat i C#. Her er spesifikasjonen. Da jeg begynte å lære meg Erlang tenkte jeg det kunne være interessant å løse den samme oppgaven i det språket.

I denne blogposten følger løsningen jeg kom opp med. Har du lest det jeg har skrevet om Erlang i det siste, og har lyst til å se mer, så kan dette være et interessant eksempel å sette seg inn i.

Cash_register.erl er en implementasjon av en veldig enkel erlang-server (gen_server). Jeg kan opprette en instans av serveren i en erlang-sesjon, og sende prosessen meldinger for å utføre operasjoner på kassaapparatet – dette tilsvarer kommandolinje-grensesnittet beskrevet i den orginale oppgaven. Du kan se et eksempel på en slik sesjon i bunn av blogposten, men først skal vi ta en gjennomgang av koden.

Den første delen definerer modulen og det eksterne interfacet. Funksjonene start og stop starter og stopper logisk nok serveren. Deretter følger funksjonene for å liste tingene butikken selger, kjøpe en (eller flere) ting, se innholdet i handlekurven, og betale. Alle disse sender en melding til prosessen ?MODULE – og ?MODULE er bare en macro som erstattes med navnet på modulen (cach_register). Prosessen lever altså i sin egen "tråd", og håndterer meldingene asynkront.

cash_register.erl, del 1:
1 -module(cash_register).
2 -behaviour(gen_server).
3 -export([start/0, stop/0, list_items/0, buy/1, buy/2,
4     show_cart/0, checkout/1]).
5 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
6     terminate/2, code_change/3]).
7 -import(lists, [sum/1]).
8
9 start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
10 stop()  -> gen_server:call(?MODULE, stop).
11
12 % main cash register interface
13
14 list_items()        -> gen_server:call(?MODULE, list_items).
15 buy(What)           -> gen_server:call(?MODULE, {buy, What, 1}).
16 buy(What, Quantity) -> gen_server:call(?MODULE, {buy, What, Quantity}).
17 show_cart()         -> gen_server:call(?MODULE, show_cart).
18 checkout(CashPayed) -> gen_server:call(?MODULE, {checkout, CashPayed}).

Og nå følger selve kassaapparat-logikken, koden som kjøres i server-prosessen og som håndterer kallene som gjøres i linje 14 til 18. Det hele er egentlig ganske enkelt, selv om det nok ser kaotisk ut ved første øyekast om man ikke er vandt til Erlang. Se på hver av handlerne og sammenlign med output når metodene kjøres (se den gule boksen i slutten av blogposten).

Spesielle ting å legge merke til: I linje 25 er det en list comprehension som lager en liste av alle tingene butikken selger kombinert med prisen for hver ting. Se også hvordan jeg oppdaterer handlekurven (State) i linje 30. I handleren som begynner på linje 39 kan du se en erlangsk switch/case, hvor jeg sjekker om det du betaler er nok for å dekke alle varene.

cash_register.erl, del 2:
20 % gen_server callbacks starts here..
21
22 init([]) -> {ok, []}. % initial State = empty shopping cart
23
24 handle_call(list_items, _From, State) ->
25   ItemsAndPrices = [{X, price(X)} || X <- [bread, milk, butter]],
26   Reply = {items_in_shop, ItemsAndPrices},
27   {reply, Reply, State};
28
29 handle_call({buy, What, Quantity}, _From, State) ->
30   State1 = [{What, Quantity}|State],
31   Price = price(What) * Quantity,
32   Reply = {bought, Quantity, What, price, Price, total, total(State1)},
33   {reply, Reply, State1};
34
35 handle_call(show_cart, _From, State) ->
36   Reply = {shopping_cart, State, total, total(State)},
37   {reply, Reply, State};
38
39 handle_call({checkout, CashPayed}, _From, State) ->
40   Total = total(State),
41   case CashPayed >= Total of
42     true ->
43       Reply = {
44         received, CashPayed,
45         total_to_pay, Total,
46         you_get_back, CashPayed - Total
47       },
48       State1 = [];
49     false ->
50       Reply = that_is_not_enough,
51       State1 = State
52   end,
53   {reply, Reply, State1};
54
55 handle_call(stop, _From, State) ->
56   {stop, normal, stopped, State}.

Du kan også legge merke til at linje 24 til og med 56 er én funksjon. Den har bare flere "innganger", og bruker mønstergjenkjenning til å finne ut hvilken inngang som skal brukes basert på hvilken melding den mottar (se blogposten mønstergjenkjenning i erlang).

Den neste lille biten er noen små detaljer som ble brukt i del 2 – nemlig definisjonen av prisene for hver vare, og metoden som summerer opp totalprisen for alt i handlekurven (her bruker jeg også en list comprehension).

cash_register.erl, del 3:
58 % cach register specific logic
59
60 price(bread-> 3.99;
61 price(milk)   -> 2.50;
62 price(butter) -> 4.00.
63
64 total(L) -> sum([price(What) * Quantity || {What, Quantity} <- L]).

De siste fem linjene er egentlig ikke i bruk, men er nødvendige for at jeg skal kunne bruke gen_server-modulen. Legg merke til metoden "code_change" – den blir kalt om man endre modulens kode mens serveren kjører!

cash_register.erl, del 4:
66 % Some more callbacks needed to satisfy gen_server interface
67 handle_cast(_Msg, State)            -> {noreply, State}.
68 handle_info(_Info, State)           -> {noreply, State}.
69 terminate(_Reason, _State)          -> ok.
70 code_change(_OldVsn, State, _Extra) -> {ok, State}.

Til slutt må jeg bevise at programmet fungerer som det skal. Ved hjelp av et interaktivt Erlang shell kan jeg kompilere modulen min (prompt 1 nedenfor), starte serveren i en ny prosess (prompt 2), og begynne å handle: 

C:\Users\tormar\erlang_projects\cash_register>erl
Eshell V5.7.4  (abort with ^G)
1> c(cash_register).
{ok,cash_register}
2> cash_register:start().
{ok,<0.38.0>}
3> cash_register:list_items().
{items_in_shop,[{bread,3.99},{milk,2.5},{butter,4.0}]}
4> cash_register:buy(butter).
{bought,1,butter,price,4.0,total,4.0}
5> cash_register:buy(bread, 2).
{bought,2,bread,price,7.98,total,11.98}
6> cash_register:buy(milk, 4).
{bought,4,milk,price,10.0,total,21.98}
7> cash_register:show_cart().
{shopping_cart,[{milk,4},{bread,2},{butter,1}],total,21.98}
8> cash_register:checkout(20).
that_is_not_enough 
9> cash_register:checkout(50).
{received,50,total_to_pay,21.98,you_get_back,28.02}
10> cash_register:show_cart(). 
{shopping_cart,[],total,0} 
11> cash_register:stop().
stopped
12>

PS til prompt 2: Det som returneres når jeg starter cash_register servicen er en Pid (process id).

Så, er det noen erfarne Erlang-utviklere der ute som vil kommentere min første gen_server? Det er sikkert mye å ta tak i her :)


comments powered by Disqus