Słowo o funkcjach anonimowych w Elixirze
Na wczorajszym spotkaniu wroc-fp mieliśmy newsa w postaci „będzie grupa elixirowa” i tak przy okazji padło pytanie, jak działa operator & w elixirze.
Podstawy
Jak wiadomo, chociażby z tego artykułu, jedną z cech praktycznego programowania funkcyjnego jest możliwość przekazywania funkcji jako parametrów i zwracania ich jako wyników. Czasami potrzebujemy wykonać pewną operację przyjmującą jako argument funkcję, ale nie mamy odpowiedniej funkcji w API. W takim wypadku mamy dwie drogi. Pierwsza to utworzenie funkcji w module wraz ze wszystkimi tego konsekwencjami np. związanymi z metrykami, czy sposobem przekazywana funkcji jako parametru. Druga to przekazanie funkcji anonimowej.
Znak & jest powiązany z tą drugą ścieżką.
Funkcja anonimowa
Funkcja anonimowa w elixirze jest definiowana za pomocą konstrukcji fn -> end i może być przypisana do zmiennej:
Listing 1. Definiowanie funkcji anonimowej
iex> multipla = fn (a, b) -> a * b end
#Function
iex> multipla.(2, 2)
4
Wywołanie takiej przypisanej funkcji jest lekko dziwne, to przez kropkę, ale ma sens, jeżeli chcemy zasygnalizować, że multipla nie jest zwykłą nazwaną funkcją, a właśnie anonimową. Jak już wspomniałem, funkcję można przekazać jako parametr i zwrócić jako wynik:
Listing 2. Funkcja anonimowa jako parametr i wynik
iex> b2 = fn (f2) -> fn (a) -> f2.(a, 2) end end
#Function
iex> multiplav2 = b2.(multipla)
#Function
iex> multiplav2.(2)
4
Działa i armaci. Jednak ten zapis ma pewną, drobną wadę. Jest słabo czytelny, gdy zaczynamy robić coś bardziej skomplikowanego.
Znak &
Odpowiedzią na problem czytelności jest użycie skrótowego zapisu wykorzystującego &:
Listing 3. Definiowanie funkcji anonimowej za pomocą &
iex> mlp = &( &1 * &2)
&:erlang.*/2
iex> mlp.(2, 2)
4
Mamy tu do czynienia z dwoma zastosowaniami znaku &. Pierwsze to oczywiście &(), które odpowiada fn -> end, czyli służy do zdefiniowania funkcji. Drugie to &1 i &2, które reprezentuje parametry funkcji. Jako że skrócona definicja funkcji nie zawiera parametrów nazwanych, to jasne jest, że musi istnieć jakiś sposób dostępu do nich. Zapis ze znakiem & jest takim właśnie sposobem.
Ograniczenia
Zapis ten ma pewne ograniczenia. Pierwszym jest brak możliwości stworzenia funkcji, która będzie wykonywać blok kodu:
Listing 4. & nie pozwala na „duże” funkcje
iex> &(
...> l =[&1, &2]
...> Enum.each(l , fn (el)-> IO.puts el end)
...> )
** (CompileError) iex: invalid args for &, block expressions are not allowed, got: (
l = [&1, &2]
Enum.each(l, fn el -> IO.puts(el) end)
)
iex> fn (a, b) ->
...> l = [a, b]
...> Enum.each(l, fn (el) -> IO.puts el end)
...> end
#Function
Jest to dobre ograniczenie. Dzięki niemu unikamy tego, co pojawiło się w Javie wraz z lambdami, czyli długich anonimowych bloków kodu nazwanych dla niepoznaki lambdami.
Drugim ograniczeniem jest brak możliwości zagnieżdżania się funkcji zapisanych za pomocą &:
Listing 5. Funkcje nie mogą się zagnieżdżać
iex> by2 = &( &( &1.(&1, 2)))
** (CompileError) iex: nested captures via & are not allowed: &(&1.(&1, 2))
(elixir) src/elixir_fn.erl:114: :elixir_fn.do_capture/4
Oczywiście to też jest rozsądne, ponieważ nie mamy możliwości określenia, który parametr przynależy do której funkcji.
Podsumowanie
Podsumowując nasze dzisiejsze rozważania, możemy powiedzieć, że Elixir pozwala na definiowanie funkcji anonimowych. Mogą one być przypisane do zmiennej, mogą być parametrem, jak i wynikiem wywołania innej funkcji. Ze względu na intensywne użycie funkcji anonimowych mamy możliwość zapisania ich w skrócony sposób z wykorzystaniem znaku &. Zapis skrócony ma pewne ograniczenia. Z drugiej strony promuje on pisanie bardzo krótkich i zwięzłych funkcji.