|
| 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 | + |
0 commit comments