Enum

Algorytmy pomagające przetwarzać kolekcje.

Enum

Moduł Enum zawiera ponad siedemdziesiąt funkcji wspomagających pracę z kolekcjami. Wszystkie kolekcje, o których dowiedzieliśmy się w poprzedniej lekcji, z wyjątkiem krotek, są przeliczalne.

W tej lekcji przyjrzymy się tylko niektórym z funkcji. Innym sposobem na zapoznanie się z dostępnymi funkcjami jest wykorzystanie iex:

iex> Enum.__info__(:functions) |> Enum.each(fn({function, arity}) ->
...>   IO.puts "#{function}/#{arity}"
...> end)
all?/1
all?/2
any?/1
any?/2
at/2
at/3
...

Mamy do dyspozycji ogromną ilość funkcji. Nie bez powodu. Programowanie funkcyjne opiera się na przetwarzaniu różnego rodzaju kolekcji. W połączeniu z innymi funkcjonalnościami Elixira, jako programiści otrzymujemy bardzo efektywne narzędzia.

Pełna lista jest dostępna w dokumentacji modułu Enum. Do leniwego przetwarzania kolekcji służy moduł Stream.

all?

Gdy chcemy użyć funkcji all?, jak i wielu innych z modułu Enum, musimy jako parametr przekazać funkcję, którą wywołamy na elementach kolekcji. Funkcja all? zwróci true, jeżeli dla wszystkich elementów nasza funkcja zwróci prawdę, w przeciwnym wypadku otrzymamy false:

iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 3 end)
false
iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) > 1 end)
true

any?

W przeciwieństwie do poprzedniej funkcja any? zwróci true, jeżeli choć dla jednego elementu nasza funkcja zwróci true:

iex> Enum.any?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 5 end)
true

chunk_every

Jeżeli chcesz podzielić kolekcję na mniejsze grupy to chunk_every/2 jest funkcją, której zapewne szukasz:

iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
[[1, 2], [3, 4], [5, 6]]

Jest dostępne kilka wersji chunk_every/4, ale nie będziemy ich zgłębiać. By dowiedzieć się więcej, zajrzyj do oficjalnej dokumentacji tej funkcji.

chunk_by

Jeżeli chcemy pogrupować elementy kolekcji na podstawie czegoś innego niż liczność, możemy użyć funkcji chunk_by/2. Jako argumenty przyjmuje ona kolekcję oraz funkcję. Grupy tworzone są na podstawie wyniku działania funkcji. Jeżeli wynik zmienia się, to tworzona jest nowa grupa, nawet jeżeli wcześniej istniała grupa dla danego wyniku funkcji. W poniższych przykładach każdy ciąg o tej samej długości jest grupowany, dopóki nie napotkamy nowego ciągu o nowej długości:

iex> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"]]
iex> Enum.chunk_by(["one", "two", "three", "four", "five", "six"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"], ["six"]]

map_every

Czasami grupowanie elementów kolekcji nie jest dokładnie tym, o co nam chodzi. W takim przypadku funkcja map_every/3 pozwoli nam na pracę z konkretnymi elementami kolekcji:

# Funkcja zostanie wywołana dla co trzeciego elementu
iex> Enum.map_every([1, 2, 3, 4, 5, 6, 7, 8], 3, fn x -> x + 1000 end)
[1001, 2, 3, 1004, 5, 6, 1007, 8]

each

Jeżeli chcemy przejść przez kolekcję bez zwracania nowej wartości, to używamy funkcji each/2:

iex> Enum.each(["one", "two", "three"], fn(s) -> IO.puts(s) end)
one
two
three
:ok

Uwaga: Funkcja each/2 zwraca atom :ok.

map

By wywołać naszą funkcję na każdym elemencie kolekcji i uzyskać nową kolekcję używamy funkcji map/2:

iex> Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end)
[-1, 0, 1, 2]

min

Funkcja min/1 znajduje najmniejszą wartość w kolekcji:

iex> Enum.min([5, 3, 0, -1])
-1

Funkcja min/2 robi dokładnie to samo, ale w przypadku, gdy kolekcja jest pusta, pozwala nam określić funkcję, która wytworzy minimalną wartość.

iex> Enum.min([], fn -> :foo end)
:foo  

max

Funkcja max/1 znajduje największą wartość w kolekcji:

iex> Enum.max([5, 3, 0, -1])
5

Funkcja max/2 jest dla max/1 tym, czym min/2 jest dla min/1:

iex> Enum.max([], fn -> :bar end)
:bar  

filter

Funkcja filter/2 pozwala nam na filtrowanie kolekcji w celu uwzględnienia tylko tych elementów, dla których funkcja anonimowa zwróci wartość true.

iex> Enum.filter([1, 2, 3, 4], fn(x) -> rem(x, 2) == 0 end)
[2, 4]

reduce

Funkcja reduce/3 pozwala na sprowadzenie kolekcji do pojedynczej wartości. By tego dokonać, możemy opcjonalnie podać akumulator (przykładowo 10), by został przekazany do naszej funkcji. Jeżeli nie podamy akumulatora, to zostanie zastąpiony przez pierwszy element kolekcji:

iex> Enum.reduce([1, 2, 3], 10, fn(x, acc) -> x + acc end)
16

iex> Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end)
6

iex> Enum.reduce(["a","b","c"], "1", fn(x,acc)-> x <> acc end)
"cba1"

sort

Sortowanie kolekcji jest bardzo proste dzięki nie jednej, a dwóm funkcjom sortowania.

Funkcja sort/1 wykorzystuje Erlangowe porównanie typów do określenia kolejności sortowania:

iex> Enum.sort([5, 6, 1, 3, -1, 4])
[-1, 1, 3, 4, 5, 6]

iex> Enum.sort([:foo, "bar", Enum, -1, 4])
[-1, 4, Enum, :foo, "bar"]

Natomiast sort/2 pozwala nam zapewnić własną funkcję sortowania:

# z naszą funkcją
iex> Enum.sort([%{:val => 4}, %{:val => 1}], fn(x, y) -> x[:val] > y[:val] end)
[%{val: 4}, %{val: 1}]

# bez naszej funkcji
iex> Enum.sort([%{:count => 4}, %{:count => 1}])
[%{count: 1}, %{count: 4}]

Dla wygody sort/2 pozwala nam przekazać :asc lub :desc jako funkcję sortującą:

Enum.sort([2, 3, 1], :desc)
[3, 2, 1]

uniq

Jeżeli chcemy usunąć duplikaty z kolekcji, możemy użyć funkcji uniq/1:

iex> Enum.uniq([1, 2, 3, 2, 1, 1, 1, 1, 1])
[1, 2, 3]

uniq_by

uniq_by/2 również usuwa duplikaty z kolekcji, jednocześnie umożliwiając przekazanie funkcji, która zostanie wykorzystana do porównania unikalności.

iex> Enum.uniq_by([%{x: 1, y: 1}, %{x: 2, y: 1}, %{x: 3, y: 3}], fn coord -> coord.y end)
[%{x: 1, y: 1}, %{x: 3, y: 3}]

Enum przy użyciu operatora przechwytywania (&)

Wiele funkcji w module Enum w Elixir przyjmuje anonimowe funkcje jako argument do pracy z każdym elementem kolekcji.

Te anonimowe funkcje są często zapisywane w skrócie przy użyciu operatora przechwytywania (&).

Oto kilka przykładów, które pokazują, jak operator przechwytywania może zostać wykorzystany do pracy z funkcjami z modułu Enum. Każda wersja jest funkcjonalnie równoważna.

Używanie operatora przechwytywania z funkcją anonimową

Poniżej znajduje się typowy przykład standardowej składni podczas przekazywania funkcji anonimowej do Enum.map/2.

iex> Enum.map([1,2,3], fn number -> number + 3 end)
[4, 5, 6]

Teraz wykorzystując operator przechwytywania (&); każda liczba z listy ([1, 2, 3]) zostaje przypisana do zmiennej &1, w momencie, gdy jest ona wykorzystywana przez funkcję mapującą.

iex> Enum.map([1,2,3], &(&1 + 3))
[4, 5, 6]

Można to dalej modyfikować, przypisując poprzednią funkcję anonimową zawierającą operator & do zmiennej, aby wykorzystać ją w funkcji Enum.map/2.

iex> plus_three = &(&1 + 3)
iex> Enum.map([1,2,3], plus_three)
[4, 5, 6]

Używanie operatora przechwytywania z funkcją nazwaną

Najpierw tworzymy nazwaną funkcję i wywołujemy ją w ramach funkcji anonimowej zdefiniowanej w Enum.map/2.

defmodule Adding do
  def plus_three(number), do: number + 3
end

iex>  Enum.map([1,2,3], fn number -> Adding.plus_three(number) end)
[4, 5, 6]

Następnie możemy dokonać refaktoryzacji, aby użyć operatora przechwytywania.

iex> Enum.map([1,2,3], &Adding.plus_three(&1))
[4, 5, 6]

Aby uzyskać najbardziej zwięzłą składnię, możemy bezpośrednio wywołać nazwaną funkcję bez jawnego przechwytywania zmiennej.

iex> Enum.map([1,2,3], &Adding.plus_three/1)
[4, 5, 6]
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!