theme | style | paginate | footer | backgroundImage | marp |
---|---|---|---|---|---|
uncover |
.small-text {
font-size: 0.75rem;
letter-spacing: 1px;
font-family: "Times New Roman", Tahoma, Verdana, sans-serif;
}
section {
font-size: 28px;
letter-spacing: 1px !important;
}
li {
font-size: 28px;
letter-spacing: 1px !important;
}
p.quote {
line-height: 38px;
}
q {
font-size: 32px;
letter-spacing: 1px !important;
}
cite {
text-align: right;
font-size: 28px;
margin-top: 12px;
margin-bottom: 128px;
}
code.language-elixir {
background: #000;
color: #f8f8f8;
}
section {
letter-spacing: 1px !important;
}
span.hljs-comment,
span.hljs-quote,
span.hljs-meta {
color: #7c7c7c;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-tag,
.hljs-name {
color: #f96a6a;
}
.hljs-attribute,
.hljs-selector-id {
color: #ffffb6;
}
.hljs-string,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition {
color: #a8ff60;
}
.hljs-subst {
color: #daefa3;
}
.hljs-regexp,
.hljs-link {
color: #e9c062;
}
.hljs-title,
.hljs-section,
.hljs-type,
.hljs-doctag {
color: #f0f05b;
}
.hljs-symbol,
.hljs-bullet,
.hljs-variable,
.hljs-template-variable,
.hljs-literal {
color: #ffc57a;
}
.hljs-number,
.hljs-deletion {
color:#ff73fd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
|
true |
Курс по Elixir 2023, ФМИ |
linear-gradient(to bottom, #E0EAFC, #CFDEF3) |
true |
- Какво представляват процесите?
- Как си комуникират процесите?
- Какво е връзка между процеси?
- Какви абстракции над процеси знаем?
- Защо Erlang не е статично типизиран?
- Тип спецификации и dialyzer
- Поведения
- OTP
Вие как мислите?
- Динамичното типизиране помага с hot code reloading (промяна на цели модули и функции runtime).
- Динамичното типизиране помага с изпращане на всякакви съобщения.
- Опит за статична система за изпращане на съобщения в езика Links.
- Erlang-ският receive не може да бъде направен в Links, понеже всеки receive ще трябва пълно да изразходва всички pattern-и за съобщения, за да минава type checker-a.
- Изпращането на съобщения допълнително ще затрудни типовата система.
- Всяка функция ще трябва да има допълнително информация за какви съобщения приема.
- Тоест вместо
A -> B
, нещо катоA ~ C -> B
, къдетоC
са видовете съобщения, на които функцията "може да приеме".
- Друг проблем би бил "смъртта", или терминирането на процес.
- Наблюдаването на процес би изисквало да може да приема точно
{:EXIT, ...}
съобщение. - С други думи, ако всеки процес има типове
(m, d)
, къдетоm
са съобщения, които приема иd
причини за смъртта на един процес, тогава само процес(a, _)
би наблюдавал процес(_, a)
, което е доста лимитиращо.
- Имало е много опити през годинитe за типизиране на Erlang.
- Един от тях е бил през 1997, воден от Simon Marlow (lead GHC dev) и Philip Wadler (haskell design, заслуги за теорията зад монадите).
- Резултатът бил този научен труд
- След като процесите и съобщенията са едни от най-важните неща за Ерланг, става ясно защо тази система не е добавена.
- Други опити за типизиране на Erlang също се провалили.
- Трябва ли да изоставим всякаква надежда?
- Има type checker за Erlang (OTP 25+) на Whatsapp : https://github.com/WhatsApp/eqwalizer
- Има и PhD проект на Guillaume Duboc с ръководител Giuseppe Castagna: https://elixir-lang.org/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/
- Има инструмент, наречен dialyzer, който се използва доста широко.
- Той идва със собствен type inference механизъм, наречен success typing.
- Построява таблици, които съдържат информация с инферирани типове на абсолютно всички функции в системата.
- Наричаме ги PLT - persistent lookup table
- Добрата страна на PLT-то е, че се билдва инкрементално.
- Няма да се опита да инферира точния тип на всеки израз.
- Ще гарантира, че типовете, които инферира са правилни.
- Ще гарантира, че грешките, които намира са наистина такива.
- Статичните типови системи отхвърлят всички програми, чиято коректност не могат да докажат, а Dialyzer дава грешка за всички програми, в които може да докаже, че има грешка.
- Повече информация тук.
- "Езикът" на dialyzer.
- Ето тук има пълен списък.
- Няма да се задържаме много над тях, но ще ги ползваме.
- Допринасят за документация (инструменти като ExDoc ги ползват)
- Текстови редактори дават hints чрез тях.
defmodule Server do
@type t :: %__MODULE__{
location: String.t(),
port: pos_integer(),
configurations: [%{atom => term()}]
}
defstruct [:location, :port, :configurations]
@spec get_location(server :: t()) :: String.t()
def get_location(%__MODULE__{location: location}), do: location
end
- Начин да разделим абстракцията от имплементацията.
- Oще документация
- module dispatch
- Проверка, че даден модул имплементира дадени функции.
- Да се абстрахираме от имплементации и да имаме алтернативни имплементации.
- Помага за тестове!
- Ако се налага - мислете за тях като интерфейсите в !нормалните езици.
defmodule Parser do
@callback encode(term) :: {:ok, String.t} | {:error, String.t}
@callback decode(String.t) :: {:ok, term} | {:error, String.t}
end
defmodule JSON do
@behaviour Parser
def encode(term), do: :implement
def decode(str), do: :implement
end
defmodule XML do
@behaviour Parser
def encode(term), do: :implement
def decode(str), do: :implement
end
Можем динамично да решаваме кога да извикаме даден модул.
defmodule Parser do
# ... rest of code ...
def encode!(implementation, term) do
implementation.encode(term)
end
end
Parser.encode!(JSON, term)
- Протоколите ни дават полиморфизъм над типове/данни.
- Поведенията ни дават динамично да включваме специфична имплементация на логика, където е нужно.
- Един вид - протоколите са поведения + логика за dynamic dispatch
Коментар от авторът на Elixir in Action Още от създателя на езика по темата
- OTP е платформата, с която се разпространява Erlang.
- Версиите на Erlang са версии на OTP.
- OTP е стандартната библиотека на Erlang.
- OTP е Open Telecom Platform, но с времето OTP се е превърнала от платформа за писане на телекомуникационни програми в нещо повече.
- Интерпретатор и компилатор на Erlang.
- Стандартните библиотеки на Erlang.
- Dialyzer.
- Mnesia - дистрибутирана база данни.
- ETS - база данни в паметта.
- Дебъгер.
- И много други...
В книгата Designing for Scalability with Erlang/OTP авторите Francesco Cesarini и Steve Vinoski дефинират OTP като три ключови компонента:
- Самият Erlang
- Множество от библиотеки и виртуалната машина
- Множество от system design principles
След OTP лекциите е силно препоръчително да спрете да спрете създавате процеси чрез spawn. Също така всички ваши процеси трябва да са OTP съвместими. Това ще им позволява:
- Да бъдат използвани в супервайзор дърво
- Грешките в тези процеси да бъдат записвани с повече детайли
Но няма често да ви се налага ръчно да имплементирате OTP-compliant частта.
Erlang/OTP идва с абстракции, които имплементират OTP-съвместими процеси.
Ние ще разгледаме три абстракции от OTP:
- GenServer
- Supervisor
- Application
Има и още:
- gen_statem - State Machines
- gen_event - Event handling
- GenStage - Producer/Consumer pipeline(не е част от OTP, но е compliant)
Ще разгледаме и базата ETS
- База данни в паметта
- Бърз и конкурентен достъп
- Работи се с tuple-и и Elixir/Erlang данни
- GenServer представлява процес.
- Представен е от модул, който предлага функции за различни, често срещани случаи при работа с процеси.
Какви функции?
- Функции за синхронна и/или асинхронна комуникация.
- Функции, които се извикват при създаване на процес или преди унищожаването му.
- Функции за нотифициране при грешки и за логване.
- С помощта на мета-програмиране GenServer е специален тип поведение.
- Задава callback функции, които имат имплементации по подразбиране.
- За да направим от един модул GenServer, използваме "use GenServer".
- Когато комбинираме GenServer-и със Supervisor процеси лесно можем да създадем fault-tolerant система.
- Добри практики за писане на процеси.
- Не се занимаваме с Boilerplate code за receive.
- Дава ни скелет за инициализация на state и работа с него синхронно и асинхронно.
- Получаваме и функции за реагиране при проблем или при нестандартни съобщения.
defmodule StateServer do
use GenServer
def start_link(initial_state, opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, initial_state, name: name)
end
def init(init_arg) do
{:ok, init_arg}
end
end
{:ok, pid} = StateServer.start_link(%{})
# С функциите в :sys можем да взаимодействам с GenServer-и и други OTP процеси.
:sys.get_state(pid)
Да си вземем стейта?
defmodule StateServerA do
use GenServer
def start_link(initial_state, opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, initial_state, name: name)
end
@impl GenServer
def init(init_arg), do: {:ok, init_arg}
@impl GenServer
def handle_call(:get, from, state) do
IO.inspect(from)
{:reply, state, state}
end
end
{:ok, pid} = StateServerA.start_link(%{})
GenServer.call(pid, :get)
#:gen.call(pid, :"$gen_call", :get, 5000)
#send(pid, {:"$gen_call", {self(), [:alias, make_ref()]}, :get})
#receive do
# msg -> IO.inspect(msg)
#end
- Асинхронни съобщения, handle_cast, handle_info и handle_continue.
- Терминиране на процеса и terminate.
- Code change и промяна на кода по времена изпълнение.
- Задава ни шаблон за процес със или без състояние.
- Дава ни лесен начин за комуникация без boilerplate.
- Дава ни различни начини да го спрем или накараме да чака, ако е нужно.
- На практика spawn/send/receive не се ползват при работа, ползва се GenServer.
- Това е процес, чиято роля е да наглежда други процеси и да се грижи за тях.
- С помощта на Supervisor, по лесен начин можем да изградим fault tolerant система.
- Идеологията около това поведение е лесна за възприемане.
- Трудното е да направим добър дизайн на такава система.
Let it crash!
- Важно е програмата да върви.
- Ако части от нея се сринат, не е проблем - нещо наблюдава тези части.
- Нещо ще се погрижи те да бъдат възстановени.
- Това нещо е Supervisor.
Подобно на GenServer, Supervisor е поведение, за което callback функциите имат имплементации по подразбиране.
defmodule SomeSupervisor do
use Supervisor
end
В модула Supervisor има код, който може:
- Да инициализира и стартира Supervisor процес.
- Да осигури, че Supervisor процесът прихваща EXIT сигнали.
- Да стартира определен списък от процеси-деца, зададени на Supervisor-а и да ги link-не към него.
Поведението Supervisor дава възможност:
- Ако някой от процесите-деца 'умре' непредвидено, Supervisor-ът ще получи сигнал и ще предприеме конкретно действие, зададено при имплементацията му.
- Ако Supervisor-ът бъде терминиран, всичките му процеси-деца биват 'убити'.
:one_for_one
- Ако наблюдаван процес 'умре', той се рестартира.
- Другите не се влияят.
- Тази стратегия е добра за процеси които нямат връзки и комуникация помежду си, които могат да загубят състоянието си без това да повлияе на другите процеси-деца на Supervisor-а им.
:one_for_all
- Ако наблюдаван процес 'умре', всички наблюдавани процеси биват 'убити' и след това всички се стартират наново.
- Обикновено тази стратегия се използва за процеси, чиито състояния зависят доста едно от друго и ако само един от тях бъде рестартиран ще е се наруши общата логина на програмата.
:rest_for_one
- Ако наблюдаван процес 'умре', всички наблюдавани процеси стартирани СЛЕД недго също 'умират'.
- Всички тези процеси, включително процесът-причина се рестартират по първоначалния си стартов ред.
- Тази стратегия е подходяща за ситуация като : процес 'A' няма зависимости, но процес 'Б' зависи от 'А', а има и процес 'В', който зависи както от 'Б', така и транзитивно от 'А'.
Supervisor: max_restarts и max_seconds
- С опцията :max_restarts задаваме колко пъти може един процес да бъде рестартиран в даден период от време.
- По подразбиране е 3.
- Ако за :max_seconds секунди пробваме да рестартираме процеса :max_restarts пъти, трябва да се откажем.
- В следващи лекции ще започнем да ги ползваме доста.
- Една библиотека или програма винаги има дърво от Supervisor-и.
- Листата на това дърво са GenServer-и, node-овете до тях - Supervisor-и.
- Това е шаблон спомагащ ни да имаме available/resilent/fault-tolerant система.
- Application е компонент в Elixir/Erlang, който може да бъде спиран и стартиран като едно цяло.
- Може да бъде използван от други Apllication-и.
- Един Application се грижи за едно supervision дърво и средата в която то върви.
- Винаги, когато виртуалната машина стартира, специален процес, наречен 'application_controller' се стартира с нея.
- Този процес стои над всички Application-и, които вървят в тази виртуална машина.
- Mожем да го наречем Supervisor на Application-ите.
- Можем да приемем application_controller процеса за коренът на дървото от процеси в един BEAM node.
- Под този 'корен' стоят различните Application-и, които са абстракция, обвиваща supervision дърво, която може да се стартира и спира като едно цяло.
- Те са като мега-процеси, управлявани от application_controller-a.
- Отвън те изглеждат като един процес за който имаме функции start и stop.
- Когато се стартира един Application се създават два специални процеса, които заедно са наречени 'application master'.
- Тези два процеса създават Application-а и стоят между application_controller процеса и Supervisor-а служещ за корен на supervision дървото.
- OTP Application поведението и логиката около него идват от Erlang/OTP.
- Application-ите се конфигурират със специален .app файл, написан на Erlang.
- Той се слага при .beam файловете, които описва и след това може да се зареди на node, който има в пътя си директорията с него и тези .beam файлове.
mix new <app_project_name> --sup
Mix файл и конфигурация на един Application
elixir -S mix run
- GenServer, Supervisor и Application ни дават blueprint за писане на Erlang/Elixir библиотеки и приложения.
- Те са набор от добри практики за работа с процеси и конфигурации как да се справяме с грешки и runtime поведене.
- Application-ите са блоковете, които изграждат един release, а release-а е това, което, когато пуснем да върви, наричаме BEAM node.
- В следващите лекции ще гоорим и ще се занимаваме много с Application, Supervisor и GenServer.