Kouzlíme s Elixirem aneb úvod do funkcionálních čar II.

Antonín Hackenberg
Blueberry_cz
Published in
4 min readMay 23, 2018

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/1
The 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!

--

--