Elixir. Aby programowanie znów było przyjemnością

Bezpłatny fragment

Wzorce i przekształcenia w języku Elixir

Dopasowywanie do wzorców

Dopasowywanie do wzorców dla danych strukturalnych

Dopasowywanie do wzorców i funkcje

Przekształcenie to zadanie numer 1

Stawanie się funkcyjnym za pomocą języka Elixir

Funkcje anonimowe

Funkcje nazwane

Przykład praktyczny

Refaktoryzacja do stylu funkcyjnego

Co wyróżnia ten kod

Równoległość w języku Elixir

Model aktora

Aktory a Elixir

Komunikaty

Monitorowanie naszego procesu

Ostatni przykład

Współbieżność to istota Elixira

Przypisy

WZORCE I PRZEKSZTAŁCENIA W JĘZYKU ELIXIR

AUTOR – Dave Thomas

Piętnaście lat temu język programowania Ruby potrafił mnie ekscytować. Od tego czasu nie znalazłem niczego równie pasjonującego.

Nie wynikało to z tego, że nie próbowałem. Analizowałem wszystkie języki w miarę ich pojawiania się, ale żaden mnie nie porwał – żaden nie sprawił, że chciałbym spędzić kilka lat na jego zgłębianiu.

A potem odkryłem Elixir. Dwukrotnie. Za pierwszym razem pomyślałem, że jest przyjemny, ale nie porywający. Ale Corey Haines zachęcił mnie do ponownego spojrzenia. I miał rację. Elixir jest wyjątkowy.

Oto co należy o nim wiedzieć:

Elixir to funkcyjny język programowania, który działa na wirtualnej maszynie Erlanga. Ma składnię przypominającą Ruby i prezentuje protokoły (do rozszerzania modułów bez zmiany ich źródła), makra i bardzo dobrą obsługę metaprogramowania. Ma wiele nowoczesnych cech dzięki wykorzystaniu doświadczeń z innych języków. Na przykład makra mają zakres leksykalny, więc metaprogramowanie nie grozi już zaburzeniem globalnego środowiska wykonawczego.

Pracując na wirtualnej maszynie (VM) Erlanga, o nazwie Beam, Elixir jest w pełni kompatybilny z istniejącym kodem Erlanga. Oznacza to, że możemy korzystać z dużej dostępności, wysokiej współbieżności i rozproszonej natury Erlanga. Oznacza to także, że możemy używać tysięcy istniejących bibliotek Erlanga, tych wbudowanych i tych innych firm. W szczególności Elixir może działać w ramach struktury OTP (OTP to zbiór użytecznych bibliotek i narzędzi Erlanga, a także akronim, który w istocie nie oznacza niczego. To po prostu OTP).

Popatrzmy na kod. Jeśli chcemy działać samodzielnie, możemy pobrać Elixira. Odpowiednie instrukcje znajdują się na witrynie Elixira[1].

Poniższy kod oblicza sumę elementów z listy.

elixir/sum.ex

defmodule MyList do

  def sum([]), do: 0

  def sum([ head | tail ]), do: head + sum(tail)

end

IO.puts MyList.sum [1,2,3] #=> 6

Nasza funkcja sum znajduje się w module o nazwie MyList. Funkcja jest zdefiniowana w dwóch klauzulach. Skąd więc Elixir wie, którą z nich uruchomić?

Dopasowywanie do wzorców

Tu wchodzi do gry dopasowywanie do wzorców. Lista parametrów z klauzuli pierwszej funkcji jest po prostu pusta. Gdy wywołujemy sum, ta klauzula będzie pasować wtedy i tylko wtedy, gdy wywołamy ją z pustą listą. Jeśli będzie pasować, funkcja zwróci 0.

Druga klauzula ma skomplikowaną listę parametrów – [head|tail]. Ten wzorzec pasuje do listy (mówią nam o tym nawiasy kwadratowe). Lista musi mieć pierwszy element (head) oraz tail (resztę listy). Jeśli przekazana lista ma tylko jeden element, początek (head) zostanie ustawiony na ten element, a koniec (tail) będzie pustą listą.

Wywołajmy więc MyList.sum z argumentem [1,2,3]. Elixir szuka pierwszej klauzuli, której wzorzec pasuje do argumentu. Pierwsza klauzula nie pasuje – argumentem nie jest pusta lista – ale druga klauzula pasuje. head zostaje ustawione na 1, a tail na [2,3]. Elixir wyznacza wartość treści, head+sum(tail), co oznacza, że funkcja sum jest rekurencyjnie wywoływana ponownie. Przy ostatnim wywołaniu pierwsza klauzula pasuje, więc nie ma kolejnego rekurencyjnego wywołania, a wynik zostaje zwrócony. Sekwencja wywołań wygląda mniej więcej tak:

sum([1,2,3])

1 + sum([2,3])

1 + 2 + sum([3])

1 + 2 + 3 + sum([])

1 + 2 + 3 + 0

Dopasowywanie do wzorców to samo sedno Elixira. W istocie jest to jedyny sposób powiązania wartości ze zmienną. Gdy piszemy coś, co przypomina nadanie wartości:

a = [1,2,3] 

Elixir próbuje dopasować do siebie lewą i prawą stronę. W tym przypadku robi to, wiążąc listę [1,2,3] ze zmienną a.

Gdy a ma tę wartość, możemy napisać:

[1,2,3] = a

A Elixir będzie całkiem zadowolony – wartość po lewej pasuje do wartości po prawej. Ale jeśli napiszemy tak:

99 = a

Elixir się poskarży, że nie może znaleźć dopasowania. Dzieje się tak, gdyż Elixir zmieni wartość powiązaną ze zmienną tylko wtedy, gdy jest ona po lewej stronie operatora dopasowania.

Dopasowywanie do wzorców dla danych strukturalnych

Dopasowywanie postępuje – podczas dopasowywania Elixir będzie szukał struktury dwóch stron:

[ a, b, c ] = [1, 2, 3]    # a = 1, b = 2, c = 3

[ head | tail ] = [1, 2, 3]  # head = 1, tail = [ 2, 3 ]

Po powiązaniu zmienna zatrzymuje tę samą wartość przez czas dopasowywania do wzorców. Tak więc kolejne dopasowywanie powiedzie się tylko wtedy, gdy zmienna list jest listą trzyelementową, gdzie pierwszy i ostatni element mają taką samą wartość:

[ a, b, a ] = list

Możemy mieszać stałe i zmienne po lewej stronie. Aby to pokazać, wprowadzimy do Elixira nowy typ danych – krotkę. Krotka jest kolekcją wartości stałej długości. Zapisujemy stałe krotki między nawiasami sześciennymi:

{ 1, 2, "cat" } { :ok, result }

(Zapis :ok w drugim wierszu kodu to symbol Elixira. Możemy go traktować jak stały łańcuch lub symbol z języka Ruby. Możemy też traktować go jako stałą, której wartością jest jej nazwa, ale może to prowadzić do stanów katatonicznych).

Wiele funkcji bibliotecznych zwraca dwuelementowe krotki. Pierwszy element to status wyniku – jeśli jest to :ok, wywołanie się powiodło, a jeśli jest to :error, nastąpiło niepowodzenie. Druga wartość to prawdziwy wynik wywołania, z większą ilością informacji na temat błędu.

Możemy użyć dopasowywania do wzorców do określenia, czy wywołanie zakończyło się powodzeniem:

{ :ok, stream } = File.open("somefile.txt")

Jeśli File.open się powiodło, to stream będzie zawierać wynik. Jeśli się nie powiodło, wzorce nie będą zgodne, a Elixir pokaże błąd wykonania. (W tej sytuacji lepiej będzie otwierać plik za pomocą File.open!(), co w przypadku niepowodzenia da więcej mówiący opis błędu).

Dopasowywanie do wzorców i funkcje

Dla przykładu: MyList.sum pokazuje, że dopasowywanie do wzorców ma zastosowanie także do wywoływania funkcji. Widać to? Parametry funkcji działają jak lewa strona dopasowywania, a przekazywane argumenty to odpowiednik prawej strony.

Oto inny przykład (z długą brodą): wyznacza wartość n-tej liczby w ciągu Fibonacciego. Zacznijmy od definicji liczb Fibonacciego:

fib(0) -> 1

fib(1) -> 1

fib(n) -> fib(n-2) + fib(n-1)

Gdy korzystamy z dopasowywania do wzorców, możemy włączyć tę specyfikację do kodu wykonywanego przy minimalnym wysiłku:

elixir/fib.ex

defmodule Demo do

  def fib(0), do: 1

  def fib(1), do: 1

  def fib(n), do: fib(n-2) + fib(n-1)

end

Elixir oferuje interakcyjną powłokę o nazwie iex. Pozwala ona nam pobawić się metodą fib:

elixir % iex fib.ex

Erlang R15B03 ...

Interactive Elixir (...)

iex(1)> Demo.fib(0)

1

iex(2)> Demo.fib(1)

1

iex(3)> Demo.fib(10)

89

Wspaniała jest w tym łatwość, z jaką przekształcamy specyfikację w wykonywalny kod. A gdy kod jest już napisany, łatwo jest go czytać i zobaczyć, co robi.

Warto odnotować coś jeszcze – w implementacji naszej funkcji nie ma instrukcji warunkowych. To także sprawia, że kod jest bardziej czytelny (i łatwiejszy w utrzymaniu). W istocie wiele dość dużych modułów Elixira zostało napisanych z niewielką liczbą instrukcji warunkowych lub bez nich.

Przekształcenie to zadanie numer 1

Można pomyśleć, że wszystko jest świetnie, ale pisanie matematycznych funkcji nie jest naszą codzienną pracą. Programowanie funkcyjne nie dotyczy jednak funkcji matematycznych.

Funkcje służą do przekształcania danych. Funkcja trygonometryczna sin przekształca wartość 90° na wartość 1,0. I to jest wskazówka.

W programowaniu nie chodzi o dane. Chodzi o przekształcanie danych. Każdy pisany przez nas program pobiera coś na wejściu i przekształca na jakieś wyjście. Wejściem może być żądanie z internetu, jakieś parametry z wiersza poleceń lub dane o pogodzie w Boice w Idaho. Czymkolwiek to będzie, nasz kod przekształca to wiele razy na drodze do uzyskania pożądanego wyniku.

Właśnie dlatego uważam, że programowanie funkcyjne jest naturalnym następcą programowania obiektowego. W programowaniu obiektowym jesteśmy cały czas zainteresowani stanem naszych danych. W programowaniu funkcyjnym skupiamy się na przekształcaniu danych. A przekształcanie jest miejscem, w którym jest wartość dodana.

W następnym rozdziale przyjrzymy się funkcjom, zarówno tym nazwanym, jak i anonimowym. I poznamy operator potokowy, który pozwala nam robić coś takiego:

request |> authenticate |> lookup_data |> format_response

A to wygląda jak magia.

PRZYPISY

[1] http://elixir-lang.org.