Kouzlíme s Elixirem aneb úvod do funkcionálních čar II.
Druhý článek vycházející z funkcionálního Blueberry MeetUpu v Praze (březen 2018). V prvním článku jsem stručně popisoval historii Elixiru, základní datové typy a práci s listem. Nyní se zaměříme blíže na pattern-matching a guardy. Většina příkladů vychází z tohoto repa na Githubu.
Pattern Matching
Pattern matching se v Elixiru využívá téměř všude. Můžeme ho využít k získání položek z mapy, listu, Keyword Listu, k podmínkám case nebo cond, nebo u funkcí, které dělají match s parametry. V Elixiru to funguje na základě vlastností, které se nazývají “bind” a “pattern-match”. Jestli si říkáte: “Kde je starý dobrý assign?”, budu vás muset zklamat. Na proměnné se v Elixiru musíme koukat trochu jinak.
Pojďme si to vysvětlit na následujícím příkladu. Na LHS (Left Hand Side) straně vidíme x
a na pravé (Right Hand Side) hodnotu 1
. Zde dochází k porovnání (pattern-match) RHS=LHS. V případě, že hodnota 1
ještě není k x
přiřazena, bind se o to postará a hodnota je tedy přiřazena k proměnné x
. V druhé části 1 = x
dochází už pouze k porovnání a vidíme, že dostaneme zpátky hodnotu 1
.
iex(1)> x = 1
1
iex(2)> 1 = x
1
Když zkusíme stejný příklad v Node.js, který využívá “assign”, dostaneme následující chybu:
antonin@ ~/projectpath $ node
> x = 1
1
> 1 = x
repl:1
1 = x
^ReferenceError: Invalid left-hand side in assignment
...
V Node.js je symbol rovnítka použit jako assign operátor a v Elixiru zase jako match/bind operátor. V jiných jazycích mohou být tyto symboly rozdílné např. :=
bývá použit pro bind a =
pro assign. Bohužel si na tyto inkonzistence musíme zvyknout.
Pojďme se podívat více na “pattern-match” a “bind”. Níže vidíme tuple s dvěma položkami a přiřazujeme ho k proměnným name
a lastname
v interaktivním shellu Elixiru.
iex(1)> { name, lastname } = {"John", "Doe"}
{"John", "Doe"}
iex(2)> IO.inspect name
"John"
U funkcí je, podle mě, využití pattern-matchingu jedno z nejviditelnějších. Ukážeme si funkci resolve_response
. Tato funkce může přijmout pouze tuple, který má jako první element atom a to s hodnotou :ok
nebo :error
.
Níže můžeme vidět zavolání obou možností. Při první exekuci nám funkce vrátí string “Success”
. Při druhé exekuci s atomem :error
nám nenastane match u první funkce a shoda přijde až u druhé. Výsledkem je správně RuntimeError,
protože jsme v druhé funkci zavolali raise.
iex(1)> PatternMatching.resolve_response({:ok, %{"name"=> "John"}})%{"name"=> "John"}iex(2)> PatternMatching.resolve_response({:error, "Bad Request"})** (RuntimeError) Bad Request
Co když žádný match nenastane? Od verze Elixiru 1.5 a Erlangu 20 najdeme vylepšený formát error zpráv. Do funkce jsme poslali parametr, který nikdy nezíská shodu. Elixir dokáže z AST získat všechny známé informace o tom, co mělo nebo nemělo nastat a vše nám ukáže. Vysvětlení funkčnosti můžete najít zde.
iex(1)> PatternMatching.resolve_response({:nothing, "No Data"})
** (FunctionClauseError) no function clause matching in PatternMatching.resolve_response/1The following arguments were given to PatternMatching.resolve_response/1:# 1
{:nothing, "No Data"}Attempted function clauses (showing 2 out of 2): def resolve_response({:ok, result})
def resolve_response({:error, reason})(elixir_magic_talk) lib/pattern_matching.ex:51: PatternMatching.resolve_response/1
Pokud nevíme všechny možnosti daného parametru, můžeme jednoduše deklarovat funkci resolve_response
potřetí, a místo rozloženého tuplu použijeme proměnnou pro daný parametr nebo podtržítko. Takto “chytíme” všechny ostatní volání této funkce s jedním parametrem. Podstatné je napsat tuto funkci jako poslední.
Reálný příklad použití pattern-matchingu může být třeba transformace Keyword Listu do Mapy.
defmodule PatternMatching do
def keyword_to_map(data) do
keyword_to_map(data, %{})
end
def keyword_to_map([{:name, name} | rest], state) do
keyword_to_map(rest, Map.put(state, "name", name))
end
def keyword_to_map([{:size, size} | rest], state) do
keyword_to_map(rest, Map.put(state, "size", size))
end
def keyword_to_map([], state) do
state
end
def keyword_to_map([_ | rest], state) do
keyword_to_map(rest, state)
end
end
Guards
Pokud potřebujeme komplexnější kontrolu parametrů, můžeme pattern-matching rozšířit o tzv. guard. Pomocí guardů můžeme kontrolovat daný datový typ jako is_atom/1
, is_float/1
, is_pid/1.
Případně používat aritmetické operace nebo další funkce jako length/1
nebo round/1.
Celý seznam najdete v dokumentaci.
Níže se můžete podívat na použití guards v praxi při parsovaní floatu do integeru.
defmodule PatternMatching do
def parse_number(num) when is_float(num) do
round(num)
end
def parse_number(num) when is_integer(num) do
num
end
def parse_number(_num), do: 0
end
Všechny příklady na pattern-matching jsou uvedeny v tomto repu. Budu rád za jakékoli připomínky do komentáře a další článek bude zaměřený na kompozici funkcí.
Happy coding!