Když se učíte Elixir, jednou narazíte na pojem Comprehensions. Říkáte si, co to do pr…. je??? Pak uvidíte klíčové slovo for a zaradujete se. Jasně, starý dobrý cyklus, jen mu funkcionální zaklínači říkají jinak, aby byli zajímaví. Nenechte se ale zmást prvním dojmem! Jde o odlišný koncept.

Není to ovšem nic, co už dávno neznáte, ačkoliv to možná není na první pohled vidět. Pokud znáte SELECT z SQL aspoň z rychlíku, asi pro vás není problém dešifrovat následující výraz:

SELECT o.jmeno FROM osoby AS o WHERE o.vek >= 18;

A teď Elixir:

jmena_plnoletych = for o <- osoby, o.vek >= 18, do: o.jmeno

Výsledkem je seznam jmen plnoletých osob, uložený v proměnné jmena_plnoletych. Pro úplnost - v případě SQL předpokládám tabulku osoby se sloupci jmeno a vek, v případě Elixir proměnnou osoby s obsahem např.:

[
  %{jmeno: "Viktor", vek: 27},
  %{jmeno: "Monika", vek: 23},
  %{jmeno: "Kája", vek: 12},
  ... atd ...
]

Je nutné si uvědomit, že comprehension, stejně jako SELECT, vrací seznam. Není to tedy smyčka, která něco dělá, ale výraz, který transformuje data. V příkladu výše transformuje seznam osob na seznam jmen osob plnoletých. Kromě transformace může tedy data zároveň filtrovat (plnoleté osoby). To však není vše. Co třeba výraz:

SELECT o.jmeno, m.okres FROM osoby AS o
  INNER JOIN mesta AS m ON o.mesto = m.nazev
  WHERE o.vek >= 18

A podobně v Elixiru:

vysledek =
  for o <- osoby, m <- mesta, o.mesto == m.nazev, o.vek >= 18 do
    {o.jmeno, m.okres}
  end

Comprehension může tedy i kombinovat data z více zdrojů, stejně jako JOIN v SQL. Příklad výše uloží do proměnné vysledek seznam tuplů jmen plnoletých osob a okresu, ve kterém žijí. Opět pro úplnost - zde předpokládám, že v SQL tabulce osoby existuje navíc sloupec mesto obsahující název města, kde osoba žije, a existenci tabulky mesta se sloupci nazev a okres. Pro Elixir pak proměnné:

osoby = [%{jmeno: "Viktor", vek: 27, mesto: "Turnov"}, ... atd ... ]
mesta = [%{nazev: "Turnov", okres: "Semily"}, ... atd ... ]

V Elixiru lze comprehensions využít například i k agregaci. Následující příklad vytváří mapu obsahující seznam jmen plnoletých osob v jednotlivých okresech:

osoby = [
  %{jmeno: "Viktor", vek: 27, mesto: "Turnov"},
  %{jmeno: "Monika", vek: 23, mesto: "Jilemnice"},
  %{jmeno: "Kája", vek: 12, mesto: "Neratovice"},
  %{jmeno: "Albert", vek: 35, mesto: "Kralupy nad Vltavou"}
]

mesta = [
  %{nazev: "Turnov", okres: "Semily"},
  %{nazev: "Jilemnice", okres: "Semily"},
  %{nazev: "Neratovice", okres: "Mělník"},
  %{nazev: "Kralupy nad Vltavou", okres: "Mělník"},
]

vysledek =
  for o <- osoby,
      m <- mesta,
      o.mesto == m.nazev,
      o.vek >= 18,
      reduce: %{} do
    okresy ->
      Map.update(okresy, m.okres, [o.jmeno], fn
        jmena_v_okrese -> [o.jmeno | jmena_v_okrese]
      end)
  end

Proměnná vysledek pak obsahuje:

%{
  "Mělník" => ["Albert"],
  "Semily" => ["Monika", "Viktor"]
}

V SQL pro stejný výsledek použiji GROUP BY:

SELECT m.okres, array_agg(o.jmeno) FROM osoby AS o
  INNER JOIN mesta AS m ON o.mesto = m.nazev
  WHERE o.vek >= 18
  GROUP BY m.okres

Kromě reduce lze v comprehensions použít i další přepínače, jako into a uniq, filtrování pomocí pattern-matchingu a jiné vychytávky (více v dokumentaci).

Nyní si můžete říct: Vždyť elixirový for je vlastně SELECT! Není - ale jde o příbuzný koncept. Až tedy v Elixiru narazíte na for, nenechte se zmást a místo cyklu si raději vzpomeňte na SQL.


Pozn. 1: Samozřejmě výše uvedené ukázky nejsou vždy nejlepším řešením problému, jde pouze o demonstraci for. Ale pro jednorázové sloučení dat například ze dvou CSV souborů obvykle není potřeba vymýšlet nic složitějšího, než “joinování” pomocí comprehensions.

Pozn. 2: Elixirové ukázky v podobě Livebooku jsou ke stažení zde.