Enum
Zbiór algorytmów służących do przetwarzania kolekcji wyliczeniowych.
Wstęp
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, należą do wyliczeniowego typu danych (ang. enumerables).
W tej lekcji omówiony będzie jedynie podzbiór dostępnych funkcji, jednak możemy zbadać je samodzielnie. Przeprowadźmy mały eksperyment w 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ą liczbę funkcji – nie bez powodu. Przetwarzanie różnego rodzaju kolekcji jest podstawą programowania funkcyjnego — w połączeniu z innymi instrumentami Elixira może to stanowić naprawdę niesamowicie potężne narzędzie w rękach programistów.
Podstawowe funkcje
Pełna lista funkcji 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 odróżnieniu od poprzedniej, funkcja any?
zwróci true
, jeżeli choć dla jednego elementu przekazana 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, chunk_every/2
jest funkcją, której prawdopodobnie 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ć — aby 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ę, a kiedy wynik zwracany przez tę funkcję zmienia się w stosunku do poprzedniego elementu, tworzona jest kolejna, nowa grupa.
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 przetworzenie każdego n-tego elementu, poczynając jednak zawsze od pierwszego:
# 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
Może być konieczne, aby przejść przez kolekcję bez zwracania nowej wartości — w takim przypadku możemy użyć 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 Elixirze 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!