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.