Skip to content

Commit 276ef87

Browse files
committed
Initial chunk of OTP Basics article.
Includes full scripts for the various stages of evolution.
0 parents  commit 276ef87

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# OTP Basics
2+
3+
I'm going to try to sketch out here, as briefly as possible, what you need to know to wrap your head around Erlang's OTP framework.
4+
I won't try to fill in all the details, but I'll build up the structure you can hang them on.
5+
6+
The place to start is with a very simple service: a key-value store.
7+
You can set values, you can get values, and that's about it.
8+
We'll use it something like this:
9+
10+
S = kvstore:start().
11+
kvstore:set(name, "Colin", S).
12+
V = kvstore:get(name, S).
13+
14+
Behind the scenes, we're spinning off an Erlang process and sending messages to it.
15+
We create an empty dictionary for its data, wrap it in a closure, and spawn it as a new process.
16+
17+
start() ->
18+
State = dict:new(),
19+
Handler = fun() -> loop(State) end,
20+
spawn(Handler).
21+
22+
set(Key, Value, Pid) ->
23+
Pid ! {set, Key, Value}.
24+
25+
get(Key, Pid) ->
26+
Pid ! {self(), {get, Key}},
27+
receive Value -> Value end.
28+
29+
The `loop` function receives these requests, either updates its data or sends back a value, and tail-recurses.
30+
31+
loop(State) ->
32+
receive
33+
{From, {get, Key}} ->
34+
{ok, Value} = dict:find(Key, State),
35+
From ! Value,
36+
loop(State);
37+
{set, Key, Value} ->
38+
NewState = dict:store(Key, Value, State),
39+
loop(NewState)
40+
end.
41+
42+
That's it for a basic, functioning service. Now let's mess with it a bit.
43+
If we look at the message passing, we see that `set` is a one-way request, and `get` is a two-way request.
44+
Let's split that logic out a bit.
45+
46+
loop(State) ->
47+
receive
48+
{From, Message} ->
49+
{NewState, Value} = handle_call(Message, State),
50+
From ! Value;
51+
Message ->
52+
NewState = handle_cast(Message, State),
53+
end,
54+
loop(NewState).
55+
56+
handle_call({get, Key}, State) ->
57+
{ok, Value} = dict:find(Key, State),
58+
{State, Value}.
59+
60+
handle_cast({set, Key, Value}, State) ->
61+
dict:store(Key, Value, State).
62+
63+
The important things here are that `loop` no longer has to know anything about the message content, and `handle_call` and `handle_cast` don't know anything about message passing.
64+
For `loop`, the message is a black box.
65+
`handle_call` and `handle_cast` are straight functions, so we can test them independently of the message passing.
66+
67+
As an aside, note that `handle_call` returns a new state, even though it doesn't change in this case.
68+
That comes in handy if we want to add an `increment` function which returns the updated value.
69+
70+
increment(Key, Pid) ->
71+
Pid ! {self(), {increment, Key}},
72+
receive Value -> Value end.
73+
74+
Then we can just add another `handle_call` clause, with no change to `loop`.
75+
76+
handle_call({increment, Key}, State) ->
77+
NewState = dict:update_counter(Key, 1, State),
78+
{ok, Value} = dict:find(Key, NewState),
79+
{NewState, Value};
80+
81+
And that interaction looks something like:
82+
83+
S = kvstore:start().
84+
kvstore:set(age, 45, S).
85+
V = kvstore:increment(age, S).
86+
87+
Now let's pull a similar job on the client functions. We'll split the message handling code into `call` and `cast`, logically enough.
88+
89+
cast(Message, Pid) ->
90+
Pid ! Message.
91+
92+
call(Message, Pid) ->
93+
Pid ! {self(), Message},
94+
receive Value -> Value end.
95+
96+
So now the client functions look like:
97+
98+
set(Key, Value, Pid) ->
99+
cast({set, Key, Value}, Pid).
100+
101+
get(Key, Pid) ->
102+
call({get, Key}, Pid).
103+
104+
increment(Key, Pid) ->
105+
call({increment, Key}, Pid).
106+
107+
Not a huge improvement, but a bit tidier.
108+
And now we give `start` a similar treatment, separating out the application-specific data initialization, and leaving just the functionality for spawning the now-generic message handling loop.
109+
110+
start() ->
111+
State = init(),
112+
Handler = fun() -> loop(State) end,
113+
spawn(Handler).
114+
115+
init() -> dict:new().
116+
117+

snippets/kv1.erl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-module(kv1).
2+
3+
-export([start/0, set/3, get/2]).
4+
5+
start() ->
6+
State = dict:new(),
7+
Handler = fun() -> loop(State) end,
8+
spawn(Handler).
9+
10+
set(Key, Value, Pid) ->
11+
Pid ! {set, Key, Value}.
12+
13+
get(Key, Pid) ->
14+
Pid ! {self(), {get, Key}},
15+
receive Value -> Value end.
16+
17+
loop(State) ->
18+
receive
19+
{From, {get, Key}} ->
20+
{ok, Value} = dict:find(Key, State),
21+
From ! Value,
22+
loop(State);
23+
{set, Key, Value} ->
24+
NewState = dict:store(Key, Value, State),
25+
loop(NewState)
26+
end.
27+

snippets/kv2.erl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-module(kv2).
2+
3+
-export([start/0, set/3, get/2, increment/2]).
4+
5+
start() ->
6+
State = dict:new(),
7+
Handler = fun() -> loop(State) end,
8+
spawn(Handler).
9+
10+
set(Key, Value, Pid) ->
11+
Pid ! {set, Key, Value}.
12+
13+
get(Key, Pid) ->
14+
Pid ! {self(), {get, Key}},
15+
receive Value -> Value end.
16+
17+
increment(Key, Pid) ->
18+
Pid ! {self(), {increment, Key}},
19+
receive Value -> Value end.
20+
21+
loop(State) ->
22+
receive
23+
{From, Message} ->
24+
{NewState, Value} = handle_call(Message, State),
25+
From ! Value;
26+
Message ->
27+
NewState = handle_cast(Message, State)
28+
end,
29+
loop(NewState).
30+
31+
handle_call({get, Key}, State) ->
32+
{ok, Value} = dict:find(Key, State),
33+
{State, Value};
34+
handle_call({increment, Key}, State) ->
35+
NewState = dict:update_counter(Key, 1, State),
36+
{ok, Value} = dict:find(Key, NewState),
37+
{NewState, Value}.
38+
39+
handle_cast({set, Key, Value}, State) ->
40+
dict:store(Key, Value, State).
41+

snippets/kv3.erl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
-module(kv3).
2+
3+
-export([start/0, set/3, get/2, increment/2]).
4+
5+
start() ->
6+
State = init(),
7+
Handler = fun() -> loop(State) end,
8+
spawn(Handler).
9+
10+
init() -> dict:new().
11+
12+
set(Key, Value, Pid) ->
13+
cast({set, Key, Value}, Pid).
14+
15+
get(Key, Pid) ->
16+
call({get, Key}, Pid).
17+
18+
increment(Key, Pid) ->
19+
call({increment, Key}, Pid).
20+
21+
cast(Message, Pid) ->
22+
Pid ! Message.
23+
24+
call(Message, Pid) ->
25+
Pid ! {self(), Message},
26+
receive Value -> Value end.
27+
28+
loop(State) ->
29+
receive
30+
{From, Message} ->
31+
{NewState, Value} = handle_call(Message, State),
32+
From ! Value;
33+
Message ->
34+
NewState = handle_cast(Message, State)
35+
end,
36+
loop(NewState).
37+
38+
handle_call({get, Key}, State) ->
39+
{ok, Value} = dict:find(Key, State),
40+
{State, Value};
41+
handle_call({increment, Key}, State) ->
42+
NewState = dict:update_counter(Key, 1, State),
43+
{ok, Value} = dict:find(Key, NewState),
44+
{NewState, Value}.
45+
46+
handle_cast({set, Key, Value}, State) ->
47+
dict:store(Key, Value, State).
48+

0 commit comments

Comments
 (0)