Dane oryginału

Original edition copyright © 2019 by Manning Publications Co. All Rights reserved. Title of English-language original: Elixir in Action by Saša Jurić, ISBN 9781617295027. Polish-language edition © 2020 by Polish Scientific Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All Rights reserved.

Przekład Jan Mazurek

Konsultacja merytoryczna Jakub Gutkowski

Projekt okładki polskiego wydania INT-MEDIA Dawid Mazur

Wydawca Wioleta Szczygielska-Dybciak

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Jolanta Kucharska

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN: Katarzyna Błaszczyk/Woblink

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2020

ISBN 978-83-01-21043-4

eBook został przygotowany na podstawie wydania papierowego z 2020 r., (wyd II (I w WN PWN))
Warszawa 2020

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl, reklama@pwn.pl

www.pwn.pl

Spis treści

Okładka

Strona tytułowa

Strona redakcyjna

Spis treści

przedmowa

podziękowania

o książce

o autorze

ilustracja na okładce

1. Pierwsze kroki

1.1. O Erlangu

1.1.1. Wysoka dostępność

1.1.2. Współbieżność Erlanga

Odporność na błędy

Skalowalność

Rozproszenie

Czas reakcji

1.1.3. Systemy serwerowe

1.1.4. Platforma programistyczna

1.2. O Elixirze

1.2.1. Uproszczenie kodu

1.2.2. Składanie funkcji

1.2.3. Szersza perspektywa

1.3. Wady

1.3.1. Prędkość

1.3.2. Ekosystem

2. Podstawy

2.1. Interaktywna powłoka

2.2. Praca ze zmiennymi

2.3. Organizacja kodu

2.3.1. Moduły

2.3.2. Funkcje

2.3.3. Arność funkcji

2.3.4. Widoczność funkcji

2.3.5. Importy i aliasy

2.3.6. Atrybuty modułu

Specyfikacje typów

2.3.7. Komentarze

2.4. Rozumienie systemu typów

2.4.1. Liczby

2.4.2. Atomy

Aliasy

Atomy jako wartości logiczne

Wartości nil i truthy

2.4.3. Krotki

2.4.4. Listy

Definicja listy rekurencyjnej

2.4.5. Niemutowalność

Modyfikowanie krotek

Modyfikowanie list

Korzyści

2.4.6. Mapy

Mapy dynamicznie zmieniające rozmiar

Dane strukturalne

2.4.7. Ciągi binarne i ciągi bitowe

2.4.8. Ciągi znaków

Stringi binarne

Listy znaków

2.4.9. Funkcja jako typy pierwszoklasowe

Domknięcia

2.4.10. Inne wbudowane typy

2.4.11. Typy wyższego rzędu

Range

Listy asocjacyjne

MapSet

Czasy i daty

2.4.12. Listy IO

2.5. Operatory

2.6. Makra

2.7. Rozumienie czasu uruchomienia

2.7.1. Moduły i funkcje w czasie uruchomienia

Nazwy modułów i atomy

Moduły Erlanga

Dynamiczne wywoływanie funkcji

2.7.2. Wystartowanie środowiska uruchomieniowego

Interaktywna powłoka

Używanie skryptów

Narzędzie mix

3. Przepływ sterowania

3.1. Dopasowanie wzorców

3.1.1. Operator dopasowujący

3.1.2. Dopasowanie krotek

3.1.3. Dopasowywanie stałych

3.1.4. Zmienne we wzorcach

3.1.5. Dopasowywanie list

3.1.6. Dopasowywanie map

3.1.7. Dopasowywanie ciągów bitowych i ciągów bajtowych

Dopasowywanie ciągów binarnych

3.1.8. Dopasowania złożone

3.1.9. Ogólne zachowanie

3.2. Dopasowanie do funkcji

3.2.1. Funkcje wielowarunkowe

3.2.2. Strażnicy

3.2.3. Lambdy wielowarunkowe

3.3. Instrukcje warunkowe

3.3.1. Rozgałęzianie z funkcjami wielowarunkowymi

3.3.2. Klasyczne konstrukcje rozgałęziania

If oraz unless

Instrukcje warunkowe

Case

3.3.3. Specjalna forma with

3.4. Pętle i iteracje

3.4.1. Iteracja z rekurencją

3.4.2. Wywołanie ogonowe

Rozpoznawanie wywołań ogonowych

Ćwiczenie

3.4.3. Funkcje wyższego rzędu

Reduce

3.4.4. Składanie list

3.4.5. Strumienie

Ćwiczenie

4. Abstrakcje danych

4.1. Tworzenie abstrakcji za pomocą modułów

4.1.1. Podstawowa abstrakcja

4.1.2. Łączenie abstrakcji

4.1.3. Strukturyzacja danych za pomocą map

4.1.4. Abstrakcje za pomocą struktur

Struktury a Mapy

Rekordy

4.1.5. Transparentność danych

4.2. Praca z danymi hierarchicznymi

4.2.1. Generowanie ID

4.2.2. Aktualizacja wpisów

Zabawa z dopasowywaniem wzorców

4.2.3. Niemutowalne hierarchiczne aktualizacje

Dostępni pomocnicy

Ćwiczenie: usuwanie wpisu

4.2.4. Aktualizacje iteracyjne

4.2.5. Ćwiczenie: importowanie z pliku

4.3. Polimorfizm z protokołami

4.3.1. Podstawy protokołów

4.3.2. Implementacja protokołu

4.3.3. Protokoły wbudowane

Collectable lista zadań

5. Podstawy współbieżności

5.1. Współbieżność w BEAM

5.2. Praca z procesami

5.2.1. Tworzenie procesów

5.2.2. Przekazywanie wiadomości

Algorytm receive

Wysyłanie synchroniczne

Zbieranie wyników kwerendy

5.3. Procesy serwerów stanowych

5.3.1. Procesy serwera

Procesy serwera są sekwencyjne

5.3.2. Utrzymywanie stanu procesu

5.3.3. Stan mutowalny

Refaktoryzacja pętli

5.3.4. Stany złożone

Podejście współbieżne a podejście funkcyjne

5.3.5. Procesy rejestrowane

5.4. Dyskusja na temat czasu uruchomienia

5.4.1. Proces jest sekwencyjny

5.4.2. Nieograniczone skrzynki pocztowe procesu

5.4.3. Współbieżność współdzielenia niczego (shared-nothing concurrency)

5.4.4. Działanie dyspozytora

6. Procesy serwera generycznego

6.1. Tworzenie procesu serwera generycznego

6.1.1. Podłączanie się pod moduły

6.1.2. Implementowanie kodu generycznego

6.1.3. Zastosowanie magazynu danych par

6.1.4. Wsparcie dla żądań asynchronicznych

6.1.5. Ćwiczenie: refaktoryzacja serwera zadań

6.2. Wykorzystanie GenServer

6.2.1. Zachowania (behaviours) OTP

6.2.2. Podłączanie się do GenServer

6.2.3. Obsługa żądań

6.2.4. Obsługa wiadomości

6.2.5. Pozostałe cechy GenServer

Sprawdzanie czasu kompilacji

Rejestracja nazw

Zatrzymywanie serwera

6.2.6. Cykl życia procesu

6.2.7. Procesy zgodne z OTP

6.2.8. Ćwiczenie: serwer zadań zasilany GenServer

7. Tworzenie współbieżnego systemu

7.1. Praca z mix

7.2. Zarządzanie wieloma listami zadań

7.2.1. Implementowanie pamięci podręcznej

7.2.2. Pisanie testów

7.2.3. Analiza zależności procesu

7.3. Utrwalanie danych

7.3.1. Kodowanie i utrwalanie

7.3.2. Używanie bazy danych

Przechowywanie danych

Odczyt danych

7.3.3. Analiza systemu

7.3.4. Podejmowanie kroków względem wąskiego gardła

Współbieżna obsługa żądań

Ograniczanie współbieżności poprzez pulę

7.3.5. Ćwiczenie: pula procesów i synchronizacja

7.4. Dyskusja na temat procesów

8. Podstawy odporności na błędy

8.1. Błędy czasu uruchomienia

8.1.1. Typy błędów

8.1.2. Obsługa błędów

8.2. Błędy w systemach współbieżnych

8.2.1. Łączenie procesów

Przechwytywanie wyjść

8.2.2. Monitory

8.3. Nadzorcy (supervisor)

8.3.1 Przygotowanie istniejącego kodu

8.3.2 Uruchamianie procesu nadzorcy

8.3.3 Specyfikacja dzieci

8.3.4 Opakowanie nadzorcy

8.3.5 Korzystanie z modułu wywołania zwrotnego

8.3.6 Łączenie wszystkich procesów

8.3.7 Częstotliwość restartów

9. Izolowanie skutków błędów

9.1. Drzewa nadzorców

9.1.1 Oddzielanie luźno powiązanych elementów

9.1.2 Rozbudowane wykrywanie procesów

9.1.3 Krotka via

9.1.4 Rejestracja procesów roboczych bazy danych

9.1.5 Nadzór nad procesami roboczymi baz danych

9.1.6 Organizacja drzewa nadzorców

Procesy zgodne z OTP

Zamykanie procesów

Unikanie restartów procesu

Strategie restartów

9.2. Dynamiczne uruchamianie procesów

9.2.1. Rejestracja serwerów ToDO

9.2.2. Nadzór dynamiczny

9.2.3. Wyszukiwanie serwerów zadań

9.2.4. Stosowanie tymczasowej strategii restartu

9.2.5. Testowanie systemu

9.3. „Daj mu się wywalić”

9.3.1. Procesy, które nie powinny ulegać awarii

9.3.2. Obsługa oczekiwanych błędów

9.3.3. Zachowanie stanu

10. Poza granicami GenServer

10.1. Zadania (tasks)

10.1.1. Zadania oczekujące

10.1.2. Zadania nieoczekujące

10.2. Agenci (agents)

10.2.1. Podstawowe zastosowania

10.2.2. Agenci i współbieżność

10.2.3. Serwer zadań oparty na agencie

10.2.4. Ograniczenia agentów

10.3. Tabele ETS

10.3.1. Podstawowe operacje

10.3.2. Magazyn danych par klucz/wartość oparty na ETS

10.3.3. Pozostałe operacje ETS

Wzorce dopasowania

Inne przypadki zastosowania ETS

Poza ETS

10.3.4. Ćwiczenie: rejestr procesów

11. Praca z komponentami

11.1. Aplikacje OTP

11.1.1. Tworzenie aplikacji za pomocą narzędzia mix

11.1.2. Zachowanie aplikacji

11.1.3. Uruchomienie aplikacji

11.1.4. Biblioteki jako aplikacje

11.1.5. Tworzenie aplikacji to-do

11.1.6. Struktura folderu aplikacji

Środowiska projektów mix

Struktura skompilowanego kodu

11.2. Praca z zależnościami

11.2.1. Dodawanie zależności

11.2.2. Adaptacja puli

11.2.3. Wizualizacja systemu

11.3. Tworzenie serwera WWW

11.3.1. Wybór zależności

11.3.2. Uruchamianie serwera

11.3.3. Obsługa żądań

11.3.4. Rozprawianie o systemie

Wydajność

Wywołania a rzuty (call vs cast)

11.4. Konfiguracja aplikacji

11.4.1. Środowisko aplikacji

11.4.2. Zmienna konfiguracja

11.4.3. Kwestie skryptu konfiguracji

12. Tworzenie systemu rozproszonego

12.1. Prymitywy rozproszenia

12.1.1. Uruchomienia klastra

12.1.2. Komunikacja między węzłami

12.1.3. Odnajdywanie procesów

Grupy procesów

12.1.4. Łącza i monitory

12.1.5. Inne usługi rozproszenia

12.2. Tworzenie klastra odpornego na błędy

12.2.1. Kompozycja klastra

12.2.2. Rozproszona pamięć podręczna zadań

Odnajdywanie serwerów zadań

Alternatywna metoda odnajdywania procesów

12.2.3. Implementacja replikowanej bazy danych

12.2.4. Testowanie systemu

12.2.5. Wykrywanie partycji

12.2.6. Systemy wysokiej niezawodności

12.3. Kwestie sieciowe

12.3.1. Nazwy węzłów

12.3.2. Ciasteczka

12.3.3. Węzły ukryte

12.3.4. Zapory sieciowe

Bezpieczeństwo

13. Uruchamianie systemu

13.1. Uruchomienie systemu przy użyciu narzędzi Elixira

13.1.1. Stosowanie komend mix i elixir

13.1.2. Uruchamianie skryptów

13.1.3. Kompilacja na potrzeby produkcji

13.2. Wydania OTP

13.2.1. Tworzenie wydania za pomocą destylarni (distillery)

13.2.2. Stosowanie wydania

13.2.3. Zawartość wydania

Skompilowane binaria

Konfiguracje

Spakowany pakiet wydania

Precyzyjny montaż wydania

13.3. Analiza zachowań systemu

13.3.1. Debugowanie

13.3.2. Logowanie

13.3.3. Interakcja z systemem

13.3.4. Śledzenie

przedmowa

W roku 2010 otrzymałem zadanie wdrożenia systemu przesyłającego częste aktualizacje do kilku tysięcy podłączonych użytkowników praktycznie w czasie rzeczywistym. Firma, w której wtedy pracowałem, korzystała głównie z Ruby on Rails, ale ja potrzebowałem czegoś bardziej odpowiedniego dla wysoko współbieżnego wyzwania. Zgodnie z sugestią dyrektora do spraw technologii skierowałem się w stronę Erlanga, wczytałem się w materiały, przygotowałem prototyp i wykonałem test obciążenia. Byłem pod wrażeniem początkowych wyników i postanowiłem wdrożyć całość w Erlangu. Kilka miesięcy później system został przekazany odbiorcy i od tego czasu nieprzerwanie działa.

Używając Erlanga, doceniłem jego możliwości przydatne w zarządzaniu tak skomplikowanym systemem. Z czasem zacząłem korzystać z Erlanga zamiast stosowanych wcześniej technologii. Upowszechniałem ten język najpierw w swojej firmie, później również podczas lokalnych wydarzeń. Pod koniec roku 2012 zacząłem prowadzić bloga „Erlangelista” (http://theerlangelist.com), za pomocą którego staram się przedstawić liczne zalety Erlanga programistom ze środowisk programowania obiektowego.

Erlang jest nietypowym językiem, zacząłem więc eksperymentować z Elixirem, mając nadzieję, że pomoże mi to w wyjaśnieniu piękna Erlanga w sposób, który współbrzmiałby z programowaniem obiektowym. Mimo że był on na wczesnym etapie rozwoju (w tamtym czasie w wersji 0.8), Elixir od razu wzbudził mój podziw swoją dojrzałością i tym, jak integrował się z Erlangiem. Wkrótce zacząłem używać Elixira do tworzenia nowych funkcji na potrzeby mojego systemu opartego na Erlangu.

Po kilku miesiącach skontaktował się ze mną Stephens z wydawnictwa Manning, pytając, czy byłbym zainteresowany napisaniem książki o Elixirze. W tym samym czasie powstawały dwie inne książki o tym języku. Po zastanowieniu doszedłem do wniosku, że jest miejsce na jeszcze jedną książkę, która potraktowałaby temat z innej perspektywy, skupiając się na współbieżności Elixira i podejściu OTP. Napisanie tej książki wymagało ode mnie dużo pracy, ale było to cenne doświadczenie.

Dwa lata po publikacji pierwszego wydania uzgodniliśmy z wydawnictwem, że powstanie również drugie wydanie. Nie różni się ono diametralnie od poprzedniego, ale uaktualnia je zgodnie z najnowszymi zmianami, jakie zaszły w Elixirze i Erlangu. Najistotniejsze poprawki zostały wprowadzone do rozdziałów 8, 9 i 10, które przeformułowano tak, aby odpowiadały nowym metodom pracy za pomocą nadzorców i rejestrów procesów.

Obecnie Elixir w praktyce jest w pełni aktualny. Przedstawia idiomatyczne techniki tworzenia systemów oprogramowania za pomocą Elixira. Mam nadzieję, że ta książka przypadnie Czytelnikom do gustu. Stanie się pełnoprawnym, bogatym w wiedzę podręcznikiem, który umożliwi programowania w tych językach!

podziękowania

Napisanie tej książki wymagało ode mnie znacznego wkładu czasu, w związku z tym ogromne podziękowania składam mojej żonie Renacie, za jej nieustające wsparcie i cierpliwość podczas długich godzin pracy i zajętych weekendów.

Chciałbym podziękować wydawnictwu Manning za sprawienie, że ta książka powstała. W szczególności dziękuję Michaelowi Stephensowi za pierwszy kontakt, Marjanowi Bace’owi za skierowanie mnie we właściwą stronę, Karen Miller, która upewniała się, że podążam właściwym torem, Aleksandarowi Dragosavljevicowi za organizację recenzji, Kevinowi Sullivanowi i Vincentowi Nordhausowi za doglądanie produkcji, Tiffany Taylor i Andy’emu Carrollowi za przełożenie mojego „angielskiego” na właściwy angielski oraz Candance Gillhoolley, Anie Romac i Christopherowi Kaufmannowi za promocję książki.

W książce uwzględniłem opinie recenzentów i czytelników, które otrzymałem od nich na wczesnym etapie. Największe podziękowania należą się Andrew Gibsonowi za bardzo użyteczne przemyślenia i wnioski oraz uratowanie mnie, kiedy utknąłem na ostatniej przeszkodzie. Chciałbym również podziękować Alexiejowi Sholikowi oraz Peterowi Mintenowi, którzy zapewnili wyśmienitą, natychmiastową techniczną informację zwrotną w trakcie procesu twórczego.

Dziękuję również Rizie Fahmi oraz wszystkim recenzentom technicznym. Al Rahimi, Alan Lenton, Alexey Galiullin, Andrew Courter, Arun Kumar, Ashad Dean, Christopher Bailey, Christopher Haupt, Clive Harber, Daniel Couper, Eoghan O’Donnell, Frederick Schiller, Gabor Laszló Hajba, George Thomas, Heather Campbell, Jeroen Benck-huijsen, Jorge Deflon, Jose Valim, Kosmas Chatzimichalis, Mafinar Khan, Mark Ryall, Mathias Polligkeit, Mohsen Mostafa Jokar, Tom Geudens, Tomer Elmalem, Ved Antani oraz Yurii Bodarev – dziękuję serdecznie.

Dziękuję też wszystkim czytelnikom, którzy kupili i przeczytali wersję MEAP (Manning Early Access Program) i podzielili się przydatnymi wnioskami. Dziękuję za analizę moich wynurzeń i czas poświęcony na podzielenie się wnikliwymi przemyśleniami.

Specjalne wyróżnienie należy się ludziom, dzięki którym istnieją Elixir i Erlang, początkowym twórcom, głównym członkom zespołu twórców oraz wszystkim tym, którzy wnieśli swój wkład. Dziękuję za stworzenie tak wspaniałych produktów, które czynią moją pracę łatwiejszą i zabawniejszą. Na koniec pragnę podziękować wszystkim członkom społeczności skupionej wokół Elixira – to najsympatyczniejsza, najbardziej przyjazna społeczność programistów, jaką kiedykolwiek spotkałem!

o książce

Elixir to współczesny, funkcjonalny język programowania, używany do tworzenia dużych, skalowalnych, rozproszonych i odpornych na błędy systemów na podstawie wirtualnej maszyny Erlanga. Chociaż język ten jest sam w sobie fascynujący, możliwe że jego największą zaletę stanowi fakt, iż jego docelową platformą jest Erlang.

Erlang powstał, aby pomóc programistom w radzeniu sobie z problemem niezawodności. Produkt ten opracowano z myślą o tworzeniu systemów telekomunikacyjnych, ale dziś używa się go w wielu dziedzinach, takich jak narzędzia służące do współpracy, systemy płatności online, systemy licytacji w czasie rzeczywistym, serwery baz danych oraz gry sieciowe. Jeżeli tworzysz system, który musi zapewnić usługę wielu użytkownikom na całym świecie, to twoim priorytetem powinno być jego stałe funkcjonowanie, bez zauważalnych przerw, niezależnie od problemów z oprogramowaniem czy sprzętem, które mogą pojawić się w czasie działania. W innym przypadku ryzykujesz znacznymi i częstymi przerwami, które zniechęcą użytkowników, a ci mogą szukać innych rozwiązań. System ulegający częstym przestojom nie jest wiarygodny ani użyteczny, tym samym nie spełnia swojej roli. Dostępność wydaje się więc bardzo cenną cechą – a Erlang pomoże to osiągnąć.

Celem Elixira jest unowocześnienie i poprawa jakości tworzenia systemów opartych na języku programowania Erlang. Elixir ma cechy różnych języków, takich jak Erlang, Clojure czy Ruby. Wraz z nim otrzymasz zestaw narzędzi ułatwiających zarządzanie projektem, testowanie, pakowanie oraz tworzenie dokumentacji. Można stwierdzić, że Elixir daje wstęp do świata Erlanga i poprawia wydajność programisty. Stosowanie Erlanga jako docelowej platformy uruchomieniowej sprawia, że systemy oparte na Elixirze mogą korzystać z bibliotek z ekosystemu Erlanga, wliczając w to sprawdzoną w boju platformę OTP dostarczaną wraz z Erlangiem.

Dla kogo jest przeznaczona ta książka

Dzięki tej książce nauczysz się, jak tworzyć systemy oparte na Elixirze. Nie jest to całościowe opracowanie Elixira ani Erlanga – nie wyczerpuje ono wszystkich niuansów tego języka ani nie obejmuje wszystkich aspektów wirtualnej maszyny Erlanga. Niektóre tematy zostały tu jedynie zasygnalizowane, inne pominięte, np. precyzja zmiennoprzecinkowa, szczegóły dotyczące Unicode, operacja wejścia/wyjścia na plikach czy testowanie jednostkowe. Chociaż są to istotne kwestie, ta książka nie skupia się na nich, a wiedzę z zakresu pominiętych tematów w razie potrzeby można poszerzyć za pomocą innych źródeł. Pominięcie lub skrótowe omówienie tych kwestii pozwoli na dokładniejsze skupienie się na ciekawszych i bardziej nietypowych obszarach. Programowanie współbieżne i to, jak pomaga ono w osiągnięciu skalowalności, odporności na błędy, rozproszeniu i dostępności, to tematy, na których skupia się ta książka.

Nawet omawiane tu metody nie zostały opracowane w pełni szczegółowo. Pominąłem pewne drobne niuanse na rzecz zwięzłości, chcąc skupić się na tym, co istotne. Moim celem nie było kompleksowe opracowanie tematu, a przedstawienie zasad leżących u podstaw i tego, jak wpisują się one w szerszy kontekst. Po lekturze tej książki powinieneś być w stanie z łatwością wyszukać i zrozumieć pozostałe szczegóły. Aby odpowiednio cię pokierować, w książce pojawiają się odniesienia i linki do ciekawych tematów.

Książka ta omawia kwestie na poziomie wyższym średnio zaawansowanym, dlatego też dobrze byłoby, gdybyś znał podstawy przed jej lekturą. Zakładam, że zajmujesz się zawodowo programowaniem i posiadasz kilka lat doświadczenia. Nie ma znaczenia, w jakim środowisku programowania się specjalizujesz: to może być Java, C#, Ruby, C++ lub jakikolwiek inny uniwersalny język programowania. Mile widziane jest doświadczenie w backendzie (po stronie serwera).

Nie musisz wiedzieć niczego o Erlangu, Elixirze czy jakiejkolwiek innej platformie programowania współbieżnego. W szczególności nie musisz wiedzieć nic o programowaniu funkcyjnym. Elixir jest językiem funkcyjnym i jeżeli twoim środowiskiem jest programowanie obiektowe, może cię on początkowo trochę wystraszyć. Jako doświadczony programista obiektowy, mogę jednak szczerze zapewnić, że nie ma się czym przejmować. Podstawowe funkcyjne założenia Elixira są względnie proste i powinny być łatwe do przyswojenia. Oczywiście programowanie funkcyjne znacznie różni się od wszystkiego, czego miałeś okazję doświadczyć w przypadku typowych języków programowania obiektowego i wymaga chwili na przyzwyczajenie się. Nie jest to też przesadnie skomplikowane. Jeżeli jesteś doświadczonym programistą, zrozumienie pojęć związanych z tym językiem nie powinno stanowić problemu.

Jaki jest układ tej książki

Książka ta została podzielona na trzy części.

Pierwsza część została poświęcona wprowadzeniu do języka Elixir. Przedstawia ona i opisuje jego podstawy, a w dalszej kolejności szczegółowiej omawia typowe zwroty programowania funkcyjnego.

Rozdział 1 – zawiera szczegółowe omówienie Erlanga i Elixira. Wskazuje korzyści wynikające ze stosowania tych technologii oraz różnice od pozostałych języków i platform.

Rozdział 2 – przedstawia fundamenty Elixira, takie jak moduły, funkcje i system typów.

Rozdział 3 – szczegółowo objaśnia dopasowywanie wzorców i jego zastosowanie w przypadku sterowania przepływem.

Rozdział 4 – opisuje tworzenie wysokopoziomowych abstrakcji na podstawie niezmiennych struktur danych.

W drugiej części został omówiony współbieżny model Erlanga ze wskazaniem jego wielu zalet, takich jak skalowalność i odporność na błędy.

Rozdział 5 – wyjaśnia współbieżny model Erlanga i przedstawia podstawy współbieżności.

Rozdział 6 – omawia procesy serwera: podstawy wysoce współbieżnych systemów Elixir oraz Erlang.

Rozdział 7 – przybliża zasady tworzenia bardziej zaawansowanych systemów współbieżnych.

Rozdział 8 – przedstawia zwroty obsługi błędów, skupiając się na błędach w systemach współbieżnych.

Rozdział 9 – zawiera szczegółowe omówienie izolowania wszelkich typów błędów oraz minimalizowania ich wpływu na funkcjonowanie.

Rozdział 10 – omawia alternatywy dla procesów serwera, które czasem mogą okazać się lepsze podczas implementowania części tworzonego systemu.

Trzecia część dotyczy funkcjonujących systemów.

Rozdział 11 – opisuje aplikacje OTP stosowane do pakowania komponentów nadających się do ponownego użytku.

Rozdział 12 – omawia systemy rozproszone przydatne w poprawie odporności na błędy oraz skalowalności.

Rozdział 13 – przedstawia różne metody przygotowywania do pracy systemu opartego na Elixirze, skupiając się na OTP.

O kodzie

Cały kod źródłowy w tej książce został zapisany ‘takim rodzajem czcionki, jak w tej linijce’, co powinno pomóc w odróżnieniu go od reszty tekstu. W wielu listingach kod jest opatrzony przypisami w celu zwrócenia uwagi na najistotniejsze pojęcia. Każdy układ kodu powinien znajdować się na jednej przypisanej mu stronie książki z dodanym zakończeniem wierszy i niewielkimi wcięciami akapitowymi.

Kod znajdujący się w tej książce został umieszczony również w repozytorium GitHub pod adresem: https://github.com/sasa1977/elixir-in-action. Jest on również dostępny do pobrania w postaci pliku ZIP ze strony wydawcy pod adresem www.manning.com/books/elixir-in-action-second-edition.

Forum związane z książką

Zakup publikacji Elixir w praktyce (według drugiego wydania angielskiego) zapewnia dostęp do prywatnego forum sieciowego prowadzonego przez Manning Publications, na którym można wstawiać komentarze na jej temat, zadawać pytania techniczne oraz otrzymać pomoc od autora i innych użytkowników. Aby uzyskać dostęp, przejdź pod adres www.manning.com/books/elixir-in-action-second-edition. Więcej o forach Manning i obowiązujących zasadach dowiesz się na https://forums.manning.com/ forums/about.

Zaangażowanie Manning Publications względem swoich odbiorców ma na celu zapewnienie przestrzeni dla konstruktywnego dialogu między poszczególnymi czytelnikami oraz między czytelnikami a autorem. Nie zobowiązuje to jednak autora do poświęcenia swojego czasu, a jego wkład jest dobrowolny (i bezpłatny). Prosimy zatem o zadawanie konstruktywnych i merytorycznych pytań.

o autorze

Saša Jurić jest programistą doświadczonym we wdrażaniu współbieżnych, przetwarzających dużą liczbę danych systemów serwerowych, aplikacji biurowych Windows oraz aplikacji kiosku. Po prawie 20 latach projektowania obiektowego zainteresował się językami Erlang i Elixir. Użył ich do stworzenia skalowalnego, odpornego na błędy serwera powiadomień HTTP wraz z pomocniczym systemem backendowym. Obecnie należy do zespołu Aircloak i używa Erlanga do stworzenia rozwiązania dla usługi typu plug-and-play na potrzeby polityki prywatności. Udziela się również na blogu poświęconym wyżej wymienionym językom programowania pod adresem http://theerlangelist.com.

ilustracja na okładce

Postać przedstawiona na okładce książki została opatrzona podpisem „Rosyjska dziewczyna”. Ilustracja pochodzi z dzieła Thomasa Jefferysa A Collection of the Dresses of Different Nations, Ancient and Modern (cztery tomy, Londyn), opublikowanego między rokiem 1757 a 1772. Z informacji zawartych na stronie tytułowej wynika, że są to ręcznie malowane miedzioryty rozjaśnione za pomocą gumy arabskiej. Thomas Jefferys (1719–1771), był określany Nadwornym Geografem Króla Jerzego III Hanowerskiego. Ten angielski kartograf zajmował się produkcją map. Ozdabiał i drukował mapy dla rządu oraz innych organów urzędowych. Wytwarzał też wszelkiego rodzaju mapy i atlasy na sprzedaż, zwłaszcza dotyczące Ameryki Północnej. Praca przy ich tworzeniu sprawiła, że Jeffreys zainteresował się strojami ludowymi i zwyczajami ludzi zamieszkujących tereny, które badał, genialnie ujmując je w tym czterotomowym zbiorze.

Fascynacja dalekimi krainami i podróże w celach rekreacyjnych były pod koniec XVIII wieku względnie nowymi zjawiskami, a zbiory takie, jak ten, stały się bardzo popularne. Przybliżały sylwetki mieszkańców dalekich krajów zarówno turystom, jak i osobom podróżującym palcem po mapie. Różnorodność graficznych przedstawień w książkach Jeffreysa wyraźnie wskazuje na wyjątkowość i indywidualizm narodów świata przed 200 laty. Od tego czasu zmienił się styl ubioru, a różnorodność panująca między regionami i krajami, tak bogata w tamtych czasach, zdążyła zaniknąć. Obecnie trudno odróżnić społeczności żyjące na różnych kontynentach. Współcześni mieszkańcy naszego globu zamienili kulturową i wizualną różnorodność na zróżnicowanie życia prywatnego, a także ciekawe życie intelektualne i techniczne.

W czasach, gdy publikacje z zakresu informatyki niewiele różnią się od siebie, wydawnictwo Manning Publications używa ilustracji wzorowanych na bogactwie różnorodności życia regionalnego sprzed dwóch wieków, przywróconej do życia dzięki ilustracjom Jefferysa, by docenić wynalazczość i inicjatywę przemysłu komputerowego.

1. Pierwsze kroki

W tym rozdziale zostaną omówione następujące tematy:

Ogólny zarys Erlanga.

Korzyści Elixira.

To początek twojej podróży przez świat Elixira i Erlanga, dwóch wydajnych i użytecznych technologii, mogących znacznie ułatwić tworzenie dużych, skalowalnych systemów. Możliwe że czytasz tę książkę, by poznać język programowania o nazwie Elixir. Musisz jednak wiedzieć, że Elixir jest oparty na Erlangu i jest od niego w dużym stopniu uzależniony, dlatego też zacznij od poznania języka programowania Erlang, w tym jego możliwości. Przyjrzyjmy się więc ogólnie Erlangowi.

1.1. O Erlangu

Erlang jest platformą programistyczną, przeznaczoną do tworzenia skalowalnych i niezawodnych systemów zapewniających stały dostęp do usługi przy krótkim przestoju ciągłości działania lub w ogóle jego braku. To odważne stwierdzenie, ale powstał on dokładnie w takim celu. W połowie lat 80. XX wieku Ericsson, szwedzki potentat na rynku telekomunikacyjnym, stworzył Erlanga w odpowiedzi na potrzebę posiadania własnego systemu telekomunikacyjnego, cechującego się takimi właściwościami, jak niezawodność, responsywność, skalowalność oraz stały dostęp. Sieć telefoniczna powinna funkcjonować bez przerwy, niezależnie od liczby jednoczesnych połączeń telefonicznych, nieprzewidzianych błędów czy aktualizacji sprzętu lub oprogramowania.

Mimo że Erlang powstał na potrzeby systemów telekomunikacyjnych, nie jest tylko i wyłącznie przeznaczony dla tej branży. Nie oferuje on jednoznacznego wsparcia pod kątem programowania telefonów, central czy innych urządzeń telekomunikacyjnych. To uniwersalna platforma programistyczna, oferująca wsparcie w kwestiach technicznych, niefunkcjonalnych trudnościach, takich jak współbieżność, skalowalność, odporność na błędy, rozproszenie i wysoka dostępność.

Pod koniec lat 80. i na początku lat 90. XX wieku, kiedy większość oprogramowania stanowiły programy biurowe, zapotrzebowanie na wysoką dostępność było ograniczone do specjalistycznych systemów, na przykład systemów telekomunikacyjnych. Dzisiaj mierzymy się z zupełnie inną sytuacją: nacisk kładziony jest na Internet i sieć, większość aplikacji działa w oparciu na systemach serwerowych przetwarzających zapytania, zagęszczających dane i przekazujących odpowiednie informacje do wielu podłączonych klientów. Popularne dziś systemy skupiają się na komunikacji i współpracy – za przykład mogą posłużyć serwisy społecznościowe, systemy zarządzania treścią, multimedia na żądanie i gry w trybie rozgrywki sieciowej.

Wszystkie te systemy wyróżniają się wspólnymi wymaganiami niefunkcyjnymi. System musi posiadać odpowiedni czas reakcji, niezależnie od liczby podłączonych klientów. Wpływ nieprzewidzianych błędów ma być minimalny, nie mogą one oddziaływać na cały system. Dopuszczalne są sporadyczne błędy żądania z powodu defektu, ale problem pojawia się, gdy cały system staje się niedostępny. Z założenia nigdy nie powinien on przestać działać lub być wyłączany, nawet podczas aktualizacji oprogramowania, lecz funkcjonować stale, zapewniając klientom dostęp do usługi.

Te cele mogą się wydawać niełatwe do osiągnięcia, ale są one kluczowe w przypadku tworzenia systemów, na których można polegać. Jeżeli system nie posiada krótkiego czasu reakcji i nie jest wiarygodny, to w końcu zawiedzie i przestanie spełniać swoją funkcję. Systemy należy więc tworzyć z myślą o ich stałej dostępności.

Erlang spełnia te wymogi. Wysoka dostępność wynika bezpośrednio z takich technicznych pojęć, jak skalowalność, odporność na błędy oraz rozproszenie. W odróżnieniu od innych współczesnych platform programistycznych te właśnie założenia stały za stworzeniem Erlanga. Zespół Ericssona, któremu przewodniczył Joe Armstrong, spędził kilka lat na projektowaniu, tworzeniu prototypów i eksperymentowaniu, zanim udało się zbudować platformę programistyczną znaną jako Erlang. W latach 90. ubiegłego wieku jej zastosowanie było ograniczone, ale dziś może z niej czerpać prawie każdy system.

W ostatnim czasie Erlang cieszy się większym zainteresowaniem. Od ponad dwóch dziesięcioleci opierają się na nim różne duże systemy, takie jak aplikacja WhatsApp, rozproszona baza danych Riak, usługa w chmurze Heroku, system automatyzacji dystrybucji Chef, kolejka komunikatów RabbitMQ, systemy finansowe i backend gier sieciowych. To sprawdzona technologia pod względem zarówno czasu, jak i skali. Na czym polega magia Erlanga? Zastanówmy się, jak może on pomóc w tworzeniu wysoko dostępnych i niezawodnych systemów.

1.1.1. Wysoka dostępność

Erlang powstał z myślą o zastosowaniu przy tworzeniu wysoko dostępnych systemów, które są cały czas podłączone do sieci i zapewniają klientom dostęp do usługi nawet w przypadku niespodziewanych okoliczności. Może się to wydawać z pozoru proste, ale jak się pewnie domyślasz, wiele rzeczy podczas projektowania może pójść nie tak. Aby stworzyć system funkcjonujący bez przerwy 24 godziny na dobę przez siedem dni w tygodniu, musisz zmierzyć się z kilkoma trudnościami technicznymi. Są to:

Odporność na błędy – Kiedy wydarzy się coś niespodziewanego, system musi funkcjonować nieprzerwanie. Czasem występują nieoczekiwane błędy, awarie, dochodzi do uszkodzeń komponentów, zrywają się połączenia lub cała maszyna przerywa pracę przy uruchomionym systemie. Należy pamiętać, aby dokładnie zlokalizować źródło awarii, przywrócić system do stanu sprzed jej wystąpieniem i zapewnić ciągłość działania usługi.

Skalowalność – System powinien działać niezależnie od obciążenia. Oczywiście, nikt nie kupuje ton sprzętu na wszelki wypadek, gdyby pewnego dnia wszyscy nagle postanowili skorzystać z jego usług. Gdy zwiększy się obciążenie usługi, powinniśmy być w stanie rozszerzyć nasz system o dodatkowe zasoby sprzętowe bez żadnej ingerencji w oprogramowanie. Teoretycznie powinno to być możliwe bez restartu systemu.

Rozproszenie – Aby system działał bez przerwy, musi on być wykonywany na wielu urządzeniach naraz. Wpływa to pozytywnie na jego ogólną stabilność: jeżeli jedno urządzenie przestanie działać, jego rolę może przejąć inne. Ponadto umożliwia to skalowanie poziome – możesz odpowiedzieć na zwiększone obciążenie, dodając do systemu więcej urządzeń, tym samym włączając jednostki robocze odpowiadające na zwiększone zapotrzebowanie.

Czas reakcji – System powinien być dość szybki i mieć krótki czas reakcji. Obsługa żądań nie powinna trwać przesadnie długo, nawet przy zwiększonym obciążeniu i w przypadku wystąpienia błędów. Przedłużające się zadania nie powinny blokować systemu lub wywierać znacznego wpływu na wydajność.

Aktualizacja w czasie rzeczywistym – W niektórych przypadkach możesz uznać, że najlepszą metodą na aktualizację oprogramowania będzie dokonanie jej bez ponownego restartu serwerów. I tak w przypadku systemów telekomunikacyjnych, aby dokonać aktualizacji oprogramowania, nie ma potrzeby przerywać już zainicjowanych połączeń.

Jeżeli uda ci się przezwyciężyć te trudności, twój system będzie wysoko dostępny i zdolny do ciągłego zapewnienia dostępu do usługi użytkownikom, niezależnie od sytuacji.

Erlang udostępnia każdemu narzędzia do radzenia sobie z tymi problemami – w tym właśnie celu powstał. System może posiadać wszystkie te właściwości i stać się wysoce dostępny dzięki możliwościom, jakie oferuje model współbieżny Erlanga.

Zastanówmy się, na czym polega współbieżność Erlanga.

1.1.2. Współbieżność Erlanga

Współbieżność stanowi kluczowy element systemów Erlanga. Praktycznie każdy większy funkcjonujący system oparty na Erlangu jest wysoce współbieżny. Nawet sam język programowania jest czasem nazywany językiem zorientowanym na współbieżność. Zamiast polegać na ciężkich wątkach i procesach systemu operacyjnego, Erlang przejmuje współbieżność (rys. 1.1).

Rysunek 1.1. Współbieżność w wirtualnej maszynie Erlanga

Podstawowym elementem współbieżności jest Proces Erlanga (którego nie należy mylić z procesami lub wątkami systemu operacyjnego). Typowe systemy Erlanga wykonują tysiące, a czasem miliony takich procesów. Wirtualna maszyna Erlanga, zwana BEAM (Bogdan/Bjorn’s Erlang Abstract Machine [urządzenie analityczne Erlang Bogdana i Bjorna]), posiada własnego dyspozytora mającego na celu rozproszenie wykonania procesu pomiędzy dostępne rdzenie CPU, tym samym starając się zrównoleglić wykonanie procesu na tyle, na ile to możliwe. To, w jaki sposób zostały zaimplementowane procesy, dostarcza szeregu korzyści.

Odporność na błędy

Procesy Erlanga są od siebie całkowicie niezależne. Nie współdzielą pamięci, a błąd jednego procesu nie spowoduje błędów innych procesów. Pomaga to wyizolować niespodziewane błędy. W związku z tym ma to tylko lokalny zasięg. Ponadto Erlang umożliwia wykrycie błędu procesu i zareagowanie na niego – zazwyczaj oznacza to rozpoczęcie nowego procesu zamiast tego, który zwrócił błąd.

Skalowalność

Z powodu braku współdzielenia pamięci procesy komunikują się między sobą poprzez wiadomości asynchroniczne.

Oznacza to, że nie istnieją złożone mechanizmy synchronizacji, takie jak blokady, muteksy czy semafory. W rezultacie znacznie łatwiej stworzyć i zrozumieć interakcję między współbieżnymi encjami.

Typowe systemy Erlanga są podzielone na wiele współbieżnych procesów, które współpracują między sobą, by dostarczyć pełną usługę. Wirtualna maszyna jest w stanie wydajnie zrównoleglić wykonanie danego procesu na tyle, na ile to możliwe. Czyni to systemy Erlanga skalowalnymi, ponieważ są one w stanie wykorzystać wszystkie dostępne rdzenie CPU.

Rozproszenie

Komunikacja między procesami zachodzi tak samo, niezależnie od tego, czy procesy te przebiegają na tej samej instancji BEAM, czy na dwóch różnych niezależnych komputerach. Oznacza to, że typowy wysoce współbieżny system, oparty na Erlangu, może być od razu rozproszony między wieloma urządzeniami. Pozwala to na skalowanie oraz pracę za pomocą klastra urządzeń dzielących to samo obciążenie. Dodatkowo wykonywanie systemu na wielu urządzeniach zwiększa jego odporność. Jeżeli jedna maszyna zawiedzie, inne mogą przejąć jej obowiązki.

Czas reakcji

Czas wykonywania jest sprofilowany tak, aby wspierać responsywność systemu. Jak już wiadomo, Erlang przejmuje wykonywanie wielu procesów poprzez wykorzystanie wyspecjalizowanych funkcji dyspozytora, które naprzemiennie wykonują wiele procesów Erlanga. Dyspozytor zarządza wywłaszczeniem – daje on krótki czas wykonania każdemu procesowi, po czym zatrzymuje go i przechodzi do kolejnego procesu. Czas wykonania jest ograniczony do minimum, dlatego też pojedynczy, długotrwały proces nie blokuje reszty systemu. Ponadto operacje wejścia/wyjścia są wewnętrznie delegowane do osobnych wątków lub – jeżeli jest dostępna – odpowiedniej usługi w systemie operacyjnym. Oznacza to, że żaden proces oczekujący na zakończenie operacji wejścia/wyjścia nie będzie blokował wykonywania innych procesów.

Nawet odśmiecanie pamięci dostosowano tak, aby wspierać responsywność systemu. Należy pamiętać, że procesy są od siebie całkowicie odizolowane i nie dzielą pamięci. Umożliwia to czyszczenie pamięci przy każdym procesie: zamiast zatrzymywania całego systemu, każdy proces jest czyszczony osobno, kiedy zajdzie taka potrzeba. Taka metoda jest znacznie szybsza i nie blokuje całego systemu. W rzeczywistości oznacza to, że wielordzeniowy system jest w stanie wykorzystać jeden rdzeń CPU do przeprowadzenia błyskawicznego oczyszczenia pamięci, gdy tymczasem pozostałe rdzenie są zajęte standardowym przetwarzaniem.

Jak widać, współbieżność jest istotnym aspektem Erlanga i wiąże się ona nie tylko ze zrównolegleniem. Dzięki stanowiącej jej podstawę implementacji współbieżność sprzyja odporności na błędy, rozproszeniu i responsywności systemu. Typowe systemy Erlanga wykonują wiele współbieżnych zadań, wykorzystując tysiące lub nawet miliony procesów. Może się to okazać wyjątkowo przydatne, gdy próbujemy tworzyć systemy serwerowe, które często da się wdrożyć w całości w Erlangu.

1.1.3. Systemy serwerowe

Erlang znajduje zastosowanie w przypadku wielu aplikacji i systemów. Stosowany jest również w aplikacjach biurowych i często wykorzystuje się go w środowiskach wbudowanych. Moim zdaniem najbardziej nadaje się do tworzenia systemów serwerowych – wykonywanych na więcej niż jednym serwerze obsługujących wielu klientów naraz. Zwrot system serwerowy wskazuje, że jest to więcej niż serwer przetwarzający zapytania. To cały system, który oprócz obsługi żądań wykonuje różne zadania w tle i zarządza stanem wewnątrz pamięci całego serwera (rys. 1.2).

System serwerowy często jest rozproszony między wieloma maszynami współpracującymi w celu wytworzenia wartości komercyjnej. Jednym z dostępnych rozwiązań jest zastosowanie różnych komponentów w różnych maszynach lub wykorzystanie niektórych komponentów na wielu serwerach, równoważąc obciążenie oraz zwiększając prawdopodobieństwo poprawnej pracy nawet w sytuacji wystąpienia błędu.

Rysunek 1.2. System serwerowy

Na tej płaszczyźnie Erlang ułatwi ci życie. Udostępnia podstawy do tworzenia kodu, który będzie współbieżny, skalowalny i rozproszony, oraz umożliwi zaimplementowanie całego systemu w Erlangu. Każdy komponent przedstawiony na rys. 1.2 może zostać zaimplementowany jako proces Erlanga, co czyni system skalowalnym, odpornym na błędy i łatwym do rozproszenia. Polegając na wykrywaniu i usuwaniu błędów – podstawowych funkcjach Erlanga – możesz zwiększyć niezawodność i przywrócić stan sprzed wystąpienia błędów.

Spójrzmy na przykład wzięty z życia. W ramach swoich obowiązków zawodowych brałem udział w tworzeniu dwóch serwerów sieciowych o podobnych wymogach technicznych: musiały zapewniać usługi dużej liczbie klientów, obsługiwać długotrwałe procesy, zarządzać stanem wewnątrz pamięci całego serwera, utrzymywać dane, które muszą przetrwać procesy systemu operacyjnego i restarty maszyny, oraz wykonywać pracę w tle. W tabeli 1.1 zostały przedstawione technologie stosowane na każdym z serwerów.

Tabela 1.1. Porównanie technologii zastosowanych w przypadku dwóch serwerów

Wymagania techniczne

Serwer A

Serwer B

Serwer HTTP

Przetwarzanie żądania

Długotrwałe żądania

Stan całego serwera

Długotrwałe dane

Praca w tle

Odtwarzanie po awarii usługi

Nginx i Phusion Passenger

Ruby on Rails

Go

Redis

Redis i MongoDB

Cron, skrypty Bash i Ruby

Upstart

Erlang

Erlang

Erlang

Erlang

Erlang

Erlang

Erlang

Serwer A jest obsługiwany za pomocą różnych technologii, w większości znanych i popularnych w środowisku. Istniały konkretne powody do zastosowania tych technologii: każda z nich została użyta w celu uzupełnienia braków innych, już obecnych w systemie technologii. Na przykład Ruby on Rails obsługuje żądania współbieżne w oddzielnych procesach systemu operacyjnego. Potrzebowaliśmy sposobu na dzielenie danych między różnymi procesami, dlatego zastosowaliśmy Redis. Podobnie MongoDB jest używany do zarządzania danymi frontendowymi, głównie informacjami związanymi z użytkownikami. Istnieje zatem powód do użycia każdej z technologii na serwerze A, ale rozwiązanie jako całość wydaje się dość skomplikowane. Nie jest ono zawarte w jednym projekcie, komponenty zostały użyte oddzielnie, a sam start systemu na maszynie programistycznej nie należy do łatwych zadań. Musieliśmy stworzyć narzędzie do uruchomienia systemów lokalnie!

Serwer B ma te same wymagania techniczne, polegając na jednej technologii i używając funkcji platformy stworzonych dokładnie do tych celów, które sprawdziły się w przypadku dużych systemów. Ponadto cały serwer jest częścią jednego projektu, który jest wykonywany przez jedną instancję BEAM – w trakcie pracy, funkcjonuje w ramach jednego procesu systemu operacyjnego, który używa zaledwie kilku wątków. Współbieżność jest w całości obsługiwana przez dyspozytora, a system jest skalowalny, responsywny oraz ma wysoką odporność na błędy. Został on wdrożony w postaci jednego projektu, dlatego też taki system znacznie łatwiej się instaluje, jest przyjazny w zarządzaniu i stosowaniu lokalnie na maszynie programistycznej.

Należy pamiętać, że narzędzia Erlanga nie są pełnowartościowymi rozwiązaniami alternatywnymi dla powszechnie stosowanych produktów, takich jak serwery WWW – web servers potocznie zwane serwerami WWW, np. Nginx, serwery baz danych jak Riak czy przechowywanie w pamięci danych typu klucz/wartość jak Redis. Jednak Erlang daje różne możliwości, nie wykluczając początkowego wykorzystania jedynie Erlanga i późniejszego zastosowania innych technologii, kiedy rozwiązanie Erlanga nie wystarcza. Ujednolica to cały system, ułatwiając jego tworzenie i obsługę.

Warto wspomnieć, że Erlang nie jest samotną wyspą. Jest on w stanie uruchomić kod C w ramach procesu i komunikować się z praktycznie każdym zewnętrznym komponentem, takim jak kolejki komunikatów, przechowywanie w pamięci wartości typu klucz/wartość oraz z zewnętrznymi bazami danych. Zatem wybierając Erlanga, nadal masz możliwość wykorzystania dostępnej na zewnątrz technologii. Możesz zastosować ją wtedy, kiedy zajdzie potrzeba, a nie dlatego, że twoja podstawowa platforma programistyczna nie daje ci narzędzi do rozwiązania twoich problemów.

Teraz, kiedy poznałeś już korzyści i mocne strony Erlanga, przyjrzyjmy się temu, czym jest Erlang.

1.1.4. Platforma programistyczna

Erlang to więcej niż tylko język programowania. Jest to w pełni rozbudowana platforma programistyczna, składająca się z czterech odrębnych części: języka, wirtualnej maszyny, frameworku oraz narzędzi.

Erlang jako język stanowi podstawę pisania kodu, który jest wykonywany na wirtualnej maszynie Erlanga. To prosty, funkcjonalny język, u którego podstaw leży współbieżność.

Kod źródłowy w Erlangu jest kompilowany do kodu bajtowego, który następnie jest wykonywany przez BEAM. I tu kryje się cała magia. Wirtualna maszyna zrównolegla twoje współbieżne programy Erlanga i zajmuje się izolowaniem procesów, rozproszeniem oraz dba o ogólną responsywność systemu.

Standardowym elementem Erlanga jest framework zwany Open Telecom Platform – OTP (otwarta platforma telekomunikacyjna). Mimo dość niefortunnej nazwy nie ma ona nic wspólnego z systemami telekomunikacyjnymi. To uniwersalny framework, który udostępnia abstrakcje na typowe zadania Erlanga:

wzorce współbieżności i rozproszenia,

wykrywanie i usuwanie błędów w systemach współbieżnych,

pakowanie kodu do bibliotek,

wdrażanie systemów,

aktualizacje w czasie rzeczywistym.

Wszystkie te zadania można wykonać bez OTP, ale nie ma to sensu. OTP sprawdziło się w boju w przypadku wielu funkcjonujących systemów i jest tak nierozerwalnie związane z Erlangiem, że trudno jednoznacznie je rozgraniczyć. Erlang jest dostępny jako Erlang/OTP. Narzędzia te używane są do wykonywania różnych typowych zadań, takich jak kompilowanie kodu Erlanga, uruchamianie instancji BEAM, tworzenie paczek do wdrożeń, obsługa interaktywnej powłoki, podłączanie do już uruchomionej instancji BEAM itd. Zarówno BEAM, jak i towarzyszące narzędzia można stosować na różnych platformach. Można ich używać na większości popularnych systemów operacyjnych, takich jak Unix, Linux czy Windows. Kod źródłowy Erlanga jest dostępny dla każdego i można go znaleźć na oficjalnej stronie internetowej (http://erlang.org) lub na repozytorium GitHub Erlanga (https://github.com/erlang/otp). Ericsson nadal zarządza procesem produkcji i wydaje nową wersję regularnie, raz w roku.

Na tym kończy się historia Erlanga. I tu pojawia się pytanie, jeżeli Erlang jest tak wspaniały, to po co ci Elixir. Odpowiedź znajdziesz w następnej części tekstu.

1.2. O Elixirze

Elixir jest językiem alternatywnym dla wirtualnej maszyny Erlanga, który pozwala na pisanie przejrzystszego, bardziej kompaktowego kodu, który odzwierciedla twoje intencje. Swoje programy piszesz w Elixirze i uruchamiasz je na BEAM.

Elixir jest otwartym oprogramowaniem, nad którym pracę rozpoczął José Valim. W odróżnieniu od Erlanga Elixir jest w większym stopniu efektem współpracy; obecnie w jego powstanie wkład ma około 700 osób. Nowe funkcje są regularnie omawiane na listach mailingowych, na GitHub, gdzie są omawiane problemy, oraz na kanale IRC freenode #elixir-lang. Ostateczne zdanie należy do José, ale cały projekt jest faktycznie efektem współpracy nad otwartym oprogramowaniem, co przyciąga ciekawą mieszankę doświadczonych weteranów Erlanga oraz utalentowanych młodych programistów. Kod źródłowy jest dostępny na repozytorium GitHub pod adresem https://github.com/elixir-lang/elixir.

Elixir bazuje na środowisku Erlang. Wynikiem kompilacji kodu źródłowego Elixira są pliki kodu bajtowego zgodnego z BEAM, mogące działać na instancji BEAM – możesz używać bibliotek Erlanga z Elixira, i odwrotnie. Nie ma takiej rzeczy w Erlangu, której nie byłbyś w stanie zrobić w Elixirze, i zazwyczaj kod Elixira jest równie sprawny, jak kod jego odpowiednika w Erlangu.

Elixir jest semantycznie zbliżony do Erlanga: wiele z jego konstrukcji językowych odnosi się bezpośrednio do odpowiednika w Erlangu, jednak posiada on kilka dodatkowych konstrukcji, które zmniejszają duplikację w kodzie oraz wymagają znacznie mniej szablonowych (rutynowych, powtarzalnych) konstrukcji. Ponadto porządkuje niektóre istotne elementy bibliotek, wprowadza lukier składniowy oraz jednolite narzędzie do tworzenia i pakowania rozwiązań. Wszystko, co da się zrobić w Erlangu, można wykonać również w Elixirze, i na odwrót. Z mojego doświadczenia wynika, że rozwiązanie w postaci Elixira jest zazwyczaj łatwiejsze do stworzenia i obsługi.

Przyjrzyjmy się temu, jak Elixir usprawnia pewne funkcje Erlanga. Zacznijmy od zbędnie powtarzalnego kodu.

1.2.1. Uproszczenie kodu

Jedną z najważniejszych zalet Elixira jest jego zdolność do znacznej redukcji zbędnego powtarzania pewnych struktur czy wyrażeń, co przekłada się na uproszczenie kodu, który łatwiej napisać i obsługiwać. Sprawdźmy, co to oznacza, zestawiając ze sobą kod Erlanga i Elixira.

Często stosowanym podstawowym elementem współbieżnych systemów Erlanga jest proces serwera. Procesy serwera można rozumieć jako coś w rodzaju współbieżnych obiektów. Przechowują one stan prywatny i potrafią wchodzić w interakcje z innymi procesami poprzez wiadomości. Będąc współbieżnymi, różne procesy mogą być wykonywane równolegle. Typowe systemy Erlanga polegają w znacznym stopniu na procesach, których mogą być tysiące, a nawet miliony.

Poniższy przykład kodu Erlanga implementuje prosty proces serwera dodający dwie liczby.

Listing 1.1. Proces serwera opartego na Erlangu dodającego dwie liczby

-module(sum_server).

-behaviour(gen_server).

-export([

start/0, sum/3,

init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,

code_change/3

]).

start() -> gen_server:start(?MODULE, [], []).

sum(Server, A, B) -> gen_server:call(Server, {sum, A, B}).

init(_) -> {ok, undefined}.

handle_call({sum, A, B}, _From, State) -> {reply, A + B, State}.

handle_cast(_Msg, State) -> {noreply, State}.

handle_info(_Info, State) -> {noreply, State}.

terminate(_Reason, _State) -> ok.

code_change(_OldVsn, State, _Extra) -> {ok, State}.

Nawet bez znajomości Erlanga można dojść do wniosku, że to pokaźnych rozmiarów kod jak na coś, co ma dodać jedynie dwie liczby. Co prawda owo dodanie jest współbieżne, ale niezależnie od tego, z powodu liczby linii kodu, trudno to ocenić. Nie jest też na pierwszy rzut oka oczywiste, do czego ten kod służy. Ponadto niełatwo taki kod napisać. Nawet po latach tworzenia w Erlangu systemów dostępnych na produkcji nadal nie jestem w stanie napisać tego bez zaglądania do dokumentacji czy kopiowania i wklejania z uprzednio napisanego kodu.

Problem Erlanga polega na tym, że tego szablonowego kodu praktycznie nie da się usunąć, nawet jeżeli wygląda identycznie w różnych miejscach (co, jak wynika z mojego doświadczenia, zachodzi w większości przypadków). Język ten nie daje możliwości pozbycia się tego szumu. Istnieje wprawdzie metoda ograniczenia powtarzalnego kodu przez zastosowanie funkcji parse transform, ale jest ona niezgrabna i trudna w zastosowaniu. W rzeczywistości programiści Erlanga piszą procesy serwera, używając powyższego wzorca.

Procesy serwera są istotnymi i często wykorzystywanymi narzędziami Erlanga, dlatego też programiści tego języka są niestety zmuszeni do pracy z tym stale kopiowanym i wklejanym powtarzalnym kodem. O dziwo, wielu programistów się do tego przyzwyczaja, najprawdopodobniej dzięki możliwościom, jakie potrafi dla nich zdziałać BEAM. Mówi się, że Erlang czyni rzeczy trudne prostymi i komplikuje rzeczy proste. Mimo to powyższy kod dowodzi, że powinna istnieć metoda na to, by zrobić to lepiej.

Spójrzmy na ten sam proces serwera, ale w przypadku Elixira.

Listing 1.2. Proces serwera dodający dwie liczby w Elixirze

defmodule SumServer do

use GenServer

def start do

GenServer.start(__MODULE__, nil)

end

def sum(server, a, b) do

GenServer.call(server, {:sum, a, b})

end

def handle_call({:sum, a, b}, _from, state) do

{:reply, a + b, state}

end

end

Ten kod jest znacznie mniejszy, a tym samym łatwiejszy do czytania i utrzymania. Zamiar, jaki za nim stoi, jest oczywistszy, a sam kod jest znacznie mniej obciążony powtarzalnym kodem. Mimo to jest on równie uniwersalny jak wersja w Erlangu. Zachowuje się dokładnie tak samo w czasie wykonywania i utrzymuje pełną semantykę. Nie ma takiej rzeczy, którą da się zrobić za pomocą wersji napisanej w Erlangu, której nie dałoby się wykonać przy użyciu odpowiednika napisanego w Elixirze.

Mimo że jest ona znacznie mniejsza, wersja procesu serwera napisana w Elixirze wciąż wydaje się zawierać zbędny kod, biorąc pod uwagę, że dodaje jedynie dwie liczby. Ten zbędny kod istnieje, ponieważ Elixir posiada związek semantyczny w stosunku 1:1 z wyjściową biblioteką Erlanga, używaną do tworzenia procesów serwera.

Jednak Elixir daje narzędzia do dalszej eliminacji tego, co możesz uznać za szum i duplikację. Dla przykładu, stworzyłem własną bibliotekę Elixira zwaną ExActor, która sprawia, że definicja procesu serwera staje się bardziej zwarta, co widać poniżej.

Listing 1.3. Proces serwera w Elixirze

defmodule SumServer do use

ExActor.GenServer

defstart start

defcall sum(a, b) do

reply(a + b)

end

end

Zamysł tego kodu powinien być oczywisty nawet dla programistów niemających doświadczenia z Elixirem. W czasie pracy kod zachowuje się prawie identycznie jak oba poprzednie. Przekształcenie, które sprawia, że ten kod tak się zachowuje, występuje w czasie kompilacji. Wszystkie trzy wersje są podobne pod względem kodu bajtowego.

UWAGA Wspominam o bibliotece ExActor jedynie dla zobrazowania, jak bardzo można upraszać kod w ramach Elixira. Nie będziesz musiał stosować tej biblioteki podczas korzystania z tej książki, ponieważ jest to zewnętrzna abstrakcja kryjąca istotne szczegóły dotyczące funkcjonowania procesów serwera. Aby w pełni wykorzystać potencjał procesów serwera, powinieneś zrozumieć, co sprawia, że działają, dlatego też dzięki tej książce nauczysz się o abstrakcjach niskiego poziomu. Kiedy zrozumiesz, jak działają procesy serwera, będziesz mógł zdecydować, czy chcesz użyć ExActor do ich implementowania.

Ta ostatnia implementacja procesu serwera działa z wykorzystaniem funkcji makr Elixira. Makro jest kodem Elixira wykonywanym w czasie kompilacji. Makra wykorzystują wewnętrzną reprezentację twojego kodu źródłowego jako wartość wejściową i tworzą alternatywną wartość wyjściową. Makra w Elixirze inspirowane są przez Lisp i nie powinny być mylone z makrami w stylu C. W odróżnieniu od makr C/C++, które działają tylko w oparciu na tekście, makra w Elixirze działają na podstawie struktury drzewa składniowego (AST), co ułatwia wykonywanie niecodziennych operacji na wartości wejściowej, by osiągnąć alternatywną wartość wyjściową. Oczywiście Elixir zapewnia konstrukcje pomocnicze ułatwiające to przekształcenie.

Przyjrzyjmy się ponownie temu, jak została zdefiniowana operacja sumy w ostatnim przykładzie:

defcall sum(a, b) do

reply(a + b)

end

Zwróć uwagę na defcall na początku. Nie ma takiego słowa kluczowego w Elixirze. Jest to niestandardowe makro tłumaczące daną definicję do takiej postaci:

def sum(server, a, b) do

GenServer.call(server, {:sum, a, b})

end

def handle_call({:sum, a, b}, _from, state) do

{:reply, a + b, state}

end

Makra są pisane w Elixirze, dlatego też są elastyczne i mają duże możliwości, pozwalając na rozszerzenie języka i wprowadzenie nowych konstrukcji wyglądających na jego integralną część. Na przykład otwarty projekt Ecto, którego celem jest wprowadzanie zapytań w stylu LINQ do Elixira, również jest obsługiwany za pomocą makr Elixira i zapewnia ekspresyjną składnię zapytania, do złudzenia przypominającą część języka:

from w in Weather,

where: w.prcp > 0 or w.prcp == nil,

select: w

Dzięki wsparciu makr oraz mądrej architekturze kompilatora większość Elixira jest pisana w Elixirze. Takie konstrukcje językowe jak if oraz unless i wsparcie dla struktur, są implementowane za pomocą makr. Jedynie możliwie najmniejsze jądro powstaje w Erlangu – reszta jest tworzona w Elixirze.

Makra Elixira to trochę czarna magia, ale umożliwiają one emitowanie nietrywialnych powtarzalnych konstrukcji językowych w czasie kompilacji i rozszerzenie języka o własne konstrukcje typu DSL.