This is my Journal for readings on Designing Elixir Systems with OTP.
Will post chapter 01 tomorrow! Stay tuned!
This is my Journal for readings on Designing Elixir Systems with OTP.
Will post chapter 01 tomorrow! Stay tuned!
Corresponding tweet for this thread:
Share link for this tweet.
Nice one TT! Another book I am interested in reading at some point!
DFTwBLWB : Do Fun things with Big Loud Worker Bees - acronym to think about layering application.
DFT = for building blocks of project
BLWB = for letting components work together.
lifecycle
) as simple as turn-off the TV and restart to fix
it!DataType | Properties | Pro | Cons |
---|---|---|---|
Atoms | are integer + constant + NOT garbage collected | use = name concepts + constant tables type lookup | NOT convert user-input to atom |
Arrays = NOT in Elixir! | - | random access is cheap + traversing O(n) + | |
Lists | NOT arrays! + singly linked-lists + | pattern matching on head is O(1) | Appending at END of lists or LARGE lists |
Maps | random access O(log n) => useful for heavily edited data | use MapSet when only Keys (uniqueness) | |
Strings | are binaries => efficiently stored + copy is a FULL copy (unlike lists) | DO NOT LET processes hold references to long string => may cause memory leaks + NOT use long strings for storing info + DONT concatenate | |
Tuples | fixed length data structure | use for tagging data + read chunks of data like csv | slow append => donât edit (if need to, use Maps) |
Functions | send function to data, NOT data to function | ||
PATTERN matching | allows multiple dimension inheritance (sort of) => better than inheritance!! |
This book is about design, and because Elixir heavily uses OTP, we must address how to construct layers around an OTP program.
We recommend the software layers: data structures, a functional core, tests, boundaries, lifecycle, and workers.
Do Fun Things with Big Loud Worker bees
D = Data structures (model the domain)
F = Functional programming (logic)
T = Testing
B = Boundaries (processes)
L = Lifecycles (supervisors)
W = Workers (pools and dependencies)
If you think of OTP as a way to encapsulate data, or even objects, youâre going to get it wrong. Elixir processes work best when they span a few modules that belong together. Breaking your processes up too finely invites integrity problems the same way that global variables do.
Question: what is an âintegrity problemâ here? Iâm not sure I can envision the problem the author is concerned about.
Example, weâd rather wrap a process around a chess game as a standalone component than have each piece in its own process, so we can enforce the integrity of the board at the game level.
The âdataâ layer has the simple data structures your functions will use.
When the data structure is right, the functions holding the algorithms that do things can seem to write themselves.
It must not have side effects, meaning it should not alter the state of its environment in any way.
A function invoked with the same inputs will always return the same outputs.
The boundary layer deals with side effects and state. This layer is where youâll deal with processes, and where youâll present your API to the outside world. In Elixir, that means OTP.
The machinery of processes, message passing, and recursion that form the heart of concurrency in Elixir systems
An API of plain functions that hides that machinery from clients
We typically call the collective machinery a server, the code that calls that server an API, and the code that calls that API a client. In OTPâs case, the server in that boundary layer is called a GenServer, which is an abbreviation for Generic Server.
Supervisors are about starting and stopping cleanly, whether you have a single server or a bunch of them. Once you can start cleanly and detect failure, you can get failover almost for free. When a customer support person says âDid you try turning it off and on again?â, they are using lifecycle to recover from failure, whether youâre working with a TV or a desktop computer program. They are making a good bet that shutting things down cleanly and starting with a known good state is a powerful way to heal broken things.
The workers are the different processes in your component. Generally, youâll start with a flat design having a single worker and decide where you need to be more sophisticated. As your design evolves you will possibly see places that you need to add workers, for cleaning up lifecycles or for concurrently dividing work. Connection pools are workers; tasks and agents can be as well.
In Elixir, the data is the good stuff.
make_ref()
to get a globally unique identifier â use these to identify things instead of numbers.__struct__
field that plain Elixir maps donât have.defstruct
is a macro@enforce_keys
to force the specification of one or more fields when creating a new struct.defmodule Animal do
defstruct type: "", legs: 4
end
def speak(%Animal{type: "dog"}), do: "Woof"
def speak(%Animal{type: "cat"}), do: "Meow"
==
<<>>
syntax. A bitstring is a contiguous sequence of bits in memory.<>
is actually a binary concatenation operator.Remember, the BEAM is a powerful toolkit â closer to an operating system than most programming language runtime environments.
In functional programming, functions canât update data in place, they must create new copies that transform data step by step
Example of the data structure used to represent a tic-tac-toe game
iex> board =
... %{
... {0,0}=>"O",{0,1}=>"",{0,2}=>"",
... {1,0}=>"",{1,1}=>"X",{1,2}=>"",
... {2,0}=>"",{2,1}=>"",{2,2}=>"",
}
iex> board[{1,1}]
"X"
iex> Map.put(board, {1,0}, "0")
account:
%{
account_number: String,
account_holder: %User{},
balance: Int,
transaction_log: [strings],
}
This structure works OK in many languages, but it is not a functional data structure.
account:
%{
account_number: String,
initial_balance: Integer,
account_holder: %User{},
transactions: [%Transaction{}],
}
transaction:
%{
change: Integer,
inserted_at: DateTime,
note: String,
%}
def balance(account_number, date_time), do: ...
lib/mastery/core/template.ex
:
defmodule Mastery.Core.Template do
defstruct ~w[name category instructions raw compiled generators checker]a
end
We use the sigil ~w
to create a list of words. Though you usually see ()
characters with this sigil, the []
characters work perfectly fine. The a
modifier means the statement will create a list of atoms instead of strings.
We can use the data structure to:
We have a known set of fields of disparate types
lib/mastery/core/question.ex
:
defmodule Mastery.Core.Question do
defstruct ~w[asked substitutions template]a
end
lib/mastery/core/response.ex
:
defmodule Mastery.Core.Response do
defstruct ~w[quiz_title template_name to email answer correct timestamp]a
end
lib/mastery/core/quiz.ex
:
defmodule Mastery.Core.Quiz do
defstruct title: nil,
mastery: 3,
templates: %{ },
used: [ ],
current_question: nil,
last_response: nil,
record: %{ },
mastered: [ ]
end
The core presents a clear, stable interface to any external code.
Pure Functions
When you group like functions together based on the data with the sole purpose of managing that kind of data, Elixir code becomes easier to code.
Some of our modules have only data and a constructor, and thatâs OK.
defmodule Mastery.Core.Response do
defstruct ~w[quiz_title template_name to email answer correct timestamp]a
def new(quiz, email, answer) do
question = quiz.current_question
template = question.template
%__MODULE__{
quiz_title: quiz.title,
template_name: template.name,
to: question.asked,
email: email,
answer: answer,
correct: template.checker.(question.substitutions, answer),
timestamp: DateTime.utc_now
}
end
end
__MODULE__
instead of typing the full name of the module because that code defaults to the current module, and protects us from refactoring code whenever we reorganize the project.Our rule of thumb is to use processes only when we need them to control execution, divide work, or store common state where functions wonât work.
Weâll need to compile templates as users create them.
defmodule Mastery.Core.Template do
defstruct ~w[name category instructions raw compiled generators checker]a
def new(fields) do
raw = Keyword.fetch!(fields, :raw) struct!(
__MODULE__,
Keyword.put(fields, :compiled, EEx.compile_string(raw))
)
end
end
defmodule Mastery.Core.Question do
alias Mastery.Core.Template
defstruct ~w[asked substitutions template]a
end
lib/mastery/core/question.ex
:
defp build_substitution({name, choices_or_generator}) do
{name, choose(choices_or_generator)}
end
defp choose(choices) when is_list(choices) do
Enum.random(choices)
end
defp choose(generator) when is_function(generator) do
generator.()
end
Joe Armstrong, one of the creators of Erlang, used to say weâre always taking the data to the code, which is really hard, when we could take the code to the data, and thatâs much easier.
When we have a concept in our code that needs a description, itâs tempting to reach for a comment.
Easier if you have single purpose functions
def tax(amount, city, state, sku), do: ...
vs
def compute_cart_tax(amount, city, state, sku), do: ...
vs
def compute_cart_tax_in_cents(taxable_cents, city, state, sku), do: ...
The pipe operator is one of the best things about Elixir:
When functions in your module also return the moduleâs struct, youâre built to pipe. Then complex multipurpose functions break down into pipes of single-purpose functions.
Plug.Conn
is a tokenEcto.Changeset
is a tokendef add_template(quiz, fields) do
template = Template.new(fields)
templates =
update_in(
quiz.templates,
[template.category],
&add_to_list_or_nil(&1, template)
)
%{quiz | templates: templates}
end
defp add_to_list_or_nil(nil, template), do: [template]
defp add_to_list_or_nil(templates, template), do: [template | templates]
We can beautifully generate a new test like this:
Quiz.new(title: "Basic math", mastery: 4)
|> add_template(fields_for_addition)
|> add_template(fields_for_subtraction)
|> add_template(fields_for_multiplication)
|> add_template(fields_for_division)
Each step moves our token with a simple transformation.
Each line of a function or method should be at the same level of abstraction.
Sometimes, code written to a single level of abstraction is longer.
Itâs worth it because the most complex logic is what weâre optimizing
You can tell a lot about a programmer by scanning code. For Elixir, this is especially true. When scanning Elixir, look for:
Skinny left margins mean decisions are often made in pattern matches instead of control structures like if, cond, and case.
Skinny left margins make single concept functions much more likely, and simplify tests.