Do you want to pick up from where you left of?
Take me there

Функции

В Elixir и много функционални езици, функциите са граждани “първи клас”. Ще научим за видовете функции в Elixir, какво ги отличава и как да ги използваме.

Анонимни функции

Както името подсказва, анонимната функция няма име. Както видяхме в урока относно Enum, те често се подават към други функции. За да дефинираме анонимна функция в Elixir се нуждаем от ключовите думи fn и end. Между тях може да дефинираме всякакъв брой параметри и имплементации на функции, разграничени от ->.

Нека погледнем един прост пример:

iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5

Съкращението &

Да се използват анонимни функции е толкова често срещано в Elixir, че за употребата им има съкращение:

iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5

Както сигурно сте се досетили, в съкратената версия параметрите ни са достъпни за нас като &1, &2, &3, и т.н.

Съпоставка с образец

Съпоставката с образец не е ограничена само до променливи в Elixir, може да се приложи и върху функции, както ще видим в тази секция.

Elixir използва съпоставката с образец, за да идентифицира първия подходящ набор от параметри и извиква съответната имплементация:

iex> handle_result = fn
...>   {:ok, result} -> IO.puts "Handling result..."
...>   {:error} -> IO.puts "An error has occurred!"
...> end

iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!

Именовани функции

Можем да дефинираме функции с имена, за да ги достъпваме на по-късен етап, тези наименовани функции се дефинират с ключовата дума def в модул. Ще научим повече за модулите в следващите уроци, засега ще се фокусираме само върху именовани функции.

Функции дефинирани в модул са достъпни и от други модули, това е изключително полезна конструкция в Elixir:

defmodule Greeter do
  def hello(name) do
    "Hello, " <> name
  end
end

iex> Greeter.hello("Sean")
"Hello, Sean"

Ако нашата имплементация на функция е само един ред, може да я скъсим допълнително с do::

defmodule Greeter do
  def hello(name), do: "Hello, " <> name
end

Въоръжени със знанието за съпоставка с образец, нека разгледаме рекурсия използвайки именовани функции:

defmodule Length do
  def of([]), do: 0
  def of([_ | tail]), do: 1 + of(tail)
end

iex> Length.of []
0
iex> Length.of [1, 2, 3]
3

Именоване на фукнции и Arity

Споменахме по-рано, че функциите се именоват с комбинация от дадено име и arity(броя аргументи). Това означава, че може да се правят неща като:

defmodule Greeter2 do
  def hello(), do: "Hello, anonymous person!"   # hello/0
  def hello(name), do: "Hello, " <> name        # hello/1
  def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
                                                # hello/2
end

iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"

Изредили сме имената на функциите в коментарите отгоре. Първата имплементация приема нула аргумента, за това е hello/0; втората приема един аргумент и е hello/1, и т.н. За разлика от предефинирани функции в някои други езици, за тези се мисли като отделни една от друга. (Съпоставката с образец, описан преди момент, се отнася само когато няколко дефиниции са осигурени за дефиниции на функции със същия брой аргументи.)

Частни функции

Когато не желаем други модули да достъпват дадена функция може да ползваме частни функции, които могат да бъдат достъпвани само от техния модул. Можем да ги дефинираме в Elixir посредством defp:

defmodule Greeter do
  def hello(name), do: phrase <> name
  defp phrase, do: "Hello, "
end

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
    Greeter.phrase()

Ограничители

Бегло споменахме ограничители в урока Контролни структури, сега ще видим как да ги приложим към именовани функции. Веднъж като Elixir е намерил подходяща функция всички съществуващи ограничители ще бъдат тествани.

В следващия пример имаме две еднакво разписани функции, разчитаме на ограничители, за да разберем коя да бъде използвана базирано на типа на аргумента:

defmodule Greeter do
  def hello(names) when is_list(names) do
    names = Enum.join(names, ", ")

    hello(names)
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

Аргументи по подразбиране

Ако искаме да имаме стойност по подразбиране за аргумент използваме синтаксиса аргумент \\ стойност:

defmodule Greeter do
  def hello(name, language_code \\ "en") do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello("Sean", "en")
"Hello, Sean"

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.hello("Sean", "es")
"Hola, Sean"

Когато комбинираме примера ни с ограничители със стойности по подразбиране, се сблъскваме с проблем. Нека видим как изглежда:

defmodule Greeter do
  def hello(names, language_code \\ "en") when is_list(names) do
    names = Enum.join(names, ", ")

    hello(names, language_code)
  end

  def hello(name, language_code \\ "en") when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

** (CompileError) iex:31: definitions with multiple clauses and default values require a header. Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b) do ... end

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end

def hello/2 has multiple clauses and defines defaults in one or more clauses
    iex:31: (module)

Elixir не харесва аргументи със стойност по подразбиране при наличие на няколко подходящи функции, това води до объркване. За да се справим с това добавяме описателна функция с нашите стойности по подразбиране:

defmodule Greeter do
  def hello(names, language_code \\ "en")

  def hello(names, language_code) when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names, language_code)
  end

  def hello(name, language_code) when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!