-
Notifications
You must be signed in to change notification settings - Fork 15
7ML7W Elixir Day 3 Spawning and Respawning
We opened the meeting with much bread and dip, as has become tradition, @adzz agrees to drive, @Tom reads us our rights, and we're off!
The chapter opens with an interview with Jose Valim (creator of elixir) that we largely skip over. One notable quote is this exchange:
Us: What’s your favorite feature?
Valim: That’s a very hard question... But if we are considering only what Elixir brings to the game, I would definitely pick protocols.
And protocols are never mentioned again*.
*(If you do want to know more, introduction, documentation, further reading)
The book jumps right in to gen_server
without much explanation of what they are, so we decide to start the meeting by diving in to the basic units of concurrency in Elixir, and a look at how they relate to gen_server
s.
Elixir uses the actor model as its model for concurrency. This is nice because having actors that receive messages is not a million miles away from having objects that receive messages (like we may be used to in OOP).
The 'actors' in Elixir are processes. Processes are not OS processes, they are very lightweight and production apps may have thousands of processes running.
We create a process like this:
spawn(fn -> "Hello" end)
The spawn function takes a 0 arity function, the body of which is what the process will do before it dies. spawn
returns a PID, or a process identifier. These are super important because identify the process so that you can send messages to them.
The work inside of spawn
is non blocking, meaning you can carry on executing while it works:
pid = spawn(fn -> :timer.sleep(900); IO.inspect("done") end); IO.inspect("Starting")
In the code above, you will see "Starting" print to the console, the "done".
Processes die when the work they are spawned with ends:
pid = spawn(fn -> "Hello" end)
Process.alive?(pid) #=> false
That means on their own, they aren't very useful. How do we know if the work actually happened? How do we retry, or get the result of the thing the process did?
To improve on this we can use recursion!
all: [GASP]
When processes are alive they can receive
messages, which means they can wait for messages to appear in their inbox. This is a blocking action. That means we can spawn
a process initialising it with a simple loop:
defmodule Start do
def loop() do
receive do
message -> IO.inspect(message)
end
loop()
end
end
pid = spawn(fn -> Start.loop() end)
Process.alive?(pid) #=> true
We can then send a message:
send(pid, "Hello")
We will see "Hello" printed twice (once is the return value from the send
function, the other is the IO.inspect
inside the receive).
This idea becomes more powerful if we add state. We do that by starting the process with state, then allowing messages to alter that state in specific ways. Lets build out a calculator. For now it will only add numbers you send it:
defmodule Calculator do
def loop(number) do
result = receive do
message -> (message + number) |> IO.inspect
end
loop(result)
end
end
pid = spawn(fn -> Calculator.loop(20) end)
send(process, 10) #=> we will see 10 printed to the console (the return value from send) then 30.
Nice! Now lets add a way to read the state without having to print to the console. To do that we need to be able to distinguish between the different kinds of messages our process will receive - the read message and the add message. We can do this with pattern matching. Lets first use pattern matching to get the add
working:
defmodule Calculator do
def loop(number) do
result = receive do
#left of the arrow is the pattern match, the right side is what we do if we get a pattern that matches
{:add, new_number} -> new_number + number
end
loop(result)
end
end
pid = spawn(fn -> Calculator.loop(20) end)
send(pid, {:add, 25})
Now for the read. In elixir everything runs in a process, so even if we just spawn
in the REPL, that happens in a process. We can get the PID of the process we are currently in by calling self()
. When we want to find out the current state of a process that we have spawn
ed the strategy is to get that process to send us a message containing its' current state. We do that by sending it our PID, and starting a receive
function where we wait for the return message:
Now we understand all the pieces to get our read function:
defmodule Calculator do
def equals(pid) do
send(pid, {:read, self()})
receive do
message -> message
end
end
def loop(number) do
result = receive do
#left of the arrow is the pattern match, the right side is what we do if we get a pattern that matches
{:add, new_number} -> new_number + number
{:read, caller_pid} -> send(caller_pid, number)
end
loop(result)
end
end
pid = spawn(fn -> Calculator.loop(20) end)
send(pid, {:add, 25})
Now we can send a :read
message to our caller like this:
Calculator.equals(pid) #=> 55
We can package all this neatly into a module like this:
defmodule Calculator do
def start(initial_value) do
spawn(fn -> loop(initial_value) end)
end
def add(pid, number) do
send(pid, {:add, number})
pid
end
def equals(pid) do
send(pid, {:equals, self()})
receive do
value -> value
end
end
defp loop(current_value) do
new_value =
receive do
{:add, value} -> value + current_value
{:read, caller_pid} -> send(caller_pid, current_value)
end
loop(new_value)
end
end
Calculator.start(0) |> Calculator.add(20) |> Calculator.equals() #=> 20
(the |>
operator takes the result of the function on the left of it and passes it as the first argument to the function on the right.)
Wow! We leave it to the reader to implement other calculator functions, and instead try to link all this back to gen_server
s
So far we can spawn a process, send it messages, and receive messages back. Now lets imagine we wanted to do the same thing somewhere else. For example, we implement a SimpleLog
module like so:
defmodule SimpleLog do
def start(initial_value) do
spawn(fn -> loop(initial_value) end)
end
def read(pid) do
send(pid, {:read, self()})
receive do
value -> value
end
end
def add(pid, value) do
send(pid, {:add, value})
pid
end
defp loop(current_log) do
new_value =
receive do
{:add, value} -> current_log <> " " <> value
{:read, caller_pid} -> send(caller_pid, current_log)
end
loop(new_value)
end
end
SimpleLog.start("hello") |> SimpleLog.add("world") |> SimpleLog.read() #=> "hello world"
If we squint we can see a lot of duplication between the Calculator
and the SimpleLog
modules. They both have this loop function, and they both want a way to interpret different messages, and do stuff based on them.
Imagine if we were to abstract this duplication! Oh the possibilities!
The good news is, someone already did, and they called it...
a...
gen_server
🎉
Think of it as being short for generic_server, with the server being the process, and the client being us sending messages to it, and waiting for responses.
[ BREAD INTERMISSION ]
If we refactor slightly we can make it more obvious how we might be able to achieve a nice abstraction for this.
defmodule SimpleLog do
def start(initial_value) do
spawn(fn -> loop(initial_value) end)
end
def read(pid) do
send(pid, {:read, self()})
receive do
value -> value
end
end
def add(pid, value) do
send(pid, {:add, value})
pid
end
defp loop(current_value) do
new_value =
receive do
message -> process_message(message, current_value)
end
loop(new_value)
end
def process_message({:add, value}, current_value), do: current_value <> " " <> value
def process_message({:read, caller_pid}, current_value), do: send(caller_pid, current_value)
end
From this angle, all we need is a way to add a starting state, a way to add implementations of process_message
and a way to send the messages we want to pattern match on in the loop.
These are the kinds of abstractions that a gen_server
provides!
[INTERMISSION]
We then decide to try and write our own gen_server, backwards engineering the example in the book. We choose fizz_buzz, the full repo can be found here https://github.com/Adzz/fizz-bex
- Home
- Documentation
- Choosing a Topic
- Shows & Tells
- Miscellaneous
- Opt Art
- Reinforcement Learning: An Introduction
- 10 Technical Papers Every Programmer Should Read (At Least Twice)
- 7 More Languages in 7 Weeks
- Lua, Day 1: The Call to Adventure
- Lua, Day 2: Tables All the Way Down
- Lua, Day 3
- Factor, Day 1: Stack On, Stack Off
- Factor, Day 2: Painting the Fence
- Factor, Day 3: Balancing on a Boat
- Elm, Day 1: Handling the Basics
- Elm, Day 2: The Elm Architecture
- Elm, Day 3: The Elm Architecture
- Elixir, Day 1: Laying a Great Foundation
- Elixir, Day 2: Controlling Mutations
- Elixir, Day 3: Spawning and Respawning
- Julia, Day 1: Resistance Is Futile
- Julia, Day 2: Getting Assimilated
- Julia, Day 3: Become One With Julia
- Minikanren, Days 1-3
- Minikanren, Einstein's Puzzle
- Idris Days 1-2
- Types and Programming Languages
- Chapter 1: Introduction
- Chapter 2: Mathematical Preliminaries
- Chapter 3: Untyped Arithmetic Expressions
- Chapter 4: An ML Implementation of Arithmetic Expressions
- Chapter 5: The Untyped Lambda-Calculus
- Chapters 6 & 7: De Bruijn Indices and an ML Implementation of the Lambda-Calculus
- Chapter 8: Typed Arithmetic Expressions
- Chapter 9: The Simply-Typed Lambda Calculus
- Chapter 10: An ML Implementation of Simple Types
- Chapter 11: Simple Extensions
- Chapter 11 Redux: Simple Extensions
- Chapter 13: References
- Chapter 14: Exceptions
- Chapter 15: Subtyping – Part 1
- Chapter 15: Subtyping – Part 2
- Chapter 16: The Metatheory of Subtyping
- Chapter 16: Implementation
- Chapter 18: Case Study: Imperative Objects
- Chapter 19: Case Study: Featherweight Java
- The New Turing Omnibus
- Errata
- Chapter 11: Search Trees
- Chapter 8: Random Numbers
- Chapter 35: Sequential Sorting
- Chapter 58: Predicate Calculus
- Chapter 27: Perceptrons
- Chapter 9: Mathematical Research
- Chapter 16: Genetic Algorithms
- Chapter 37: Public Key Cryptography
- Chapter 6: Game Trees
- Chapter 5: Gödel's Theorem
- Chapter 34: Satisfiability (also featuring: Sentient)
- Chapter 44: Cellular Automata
- Chapter 47: Storing Images
- Chapter 12: Error-Correcting Codes
- Chapter 32: The Fast Fourier Transform
- Chapter 36: Neural Networks That Learn
- Chapter 41: NP-Completeness
- Chapter 55: Iteration and Recursion
- Chapter 19: Computer Vision
- Chapter 61: Searching Strings
- Chapter 66: Church's Thesis
- Chapter 52: Text Compression
- Chapter 22: Minimum spanning tree
- Chapter 64: Logic Programming
- Chapter 60: Computer Viruses
- Show & Tell
- Elements of Computing Systems
- Archived pages