Модули и функции

В Эликсире принято объединять функции в модули. Мы уже использовали много разных модулей в предыдущих главах, например модуль String:

iex> String.length("hello")
5

Чтобы создать свой модуль в Эликсире, используется макрос defmodule. А для объявления функций в этом модуле используется макрос def:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

В следующих главах примеры будут больше, и будет не очень удобно набирать их в консоли. Самое время научиться компилировать код и запускать скрипты на Эликсире.

Компиляция

Как правило, очень удобно писать модули в файлы, так их можно скомпилировать и использовать повторно. Допустим, у нас есть файл с именем math.ex со следующим содержимым:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

Этот файл может быть скомпилирован, используя elixirc:

$ elixirc math.ex

Данная команда создаст файл Elixir.Math.beam, содержащий байткод описанного модуля. Если мы снова запустим iex снова, модуль будет доступен (iex должен быть запущен в той же директории, где лежит файл с байткодом):

iex> Math.sum(1, 2)
3

Проекты на Эликсире обычно организованы в три директории:

  • ebin – содержит скомпилированный байткод;
  • lib – содержит код на Эликсире (как правило файлы .ex);
  • test – содержит тесты (обычно файлы .exs).

Во время работы над реальными проектам, инструмент сборки mix будет отвечать за компиляцию и настройки правильных путей вместо вас. Для образовательных целей Эликсир также поддерживает скриптовый режим, более гибкий и без генерации каких-либо скомпилированных артефактов.

Скриптовый режим

В дополнение к расширению файлов .ex в Эликсире также поддерживаются файлы с расширением.exs для написания скриптов. C точки зрения Эликсира такие файлы идентичны, разница лишь в их назначении. .ex файлы будут скомпилированы, в то время как .exs файлы используются для скриптов. При исполнении, оба расширения компилируются и загружаются свои модули в память, но только .ex файлы записывают свой байткод на диск в формате .beam.

Например, мы можем создать файл с названием math.exs:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

И выполнить его:

$ elixir math.exs

Файл скомпилируется в оперативную память и выполнится, напечатает результат 3. Файл с байткодом не будет создан. В следующих примерах мы рекомендуем вам писать ваш код в скриптовые файлы и выполнять их как показано выше.

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

Внутри модуля мы можем определить функцию при помощи макроса def/2 и приватную функцию, используя макрос defp/2. Функция, определённая через макрос def/2 может быть вызвана из другого модуля, а приватная функция может быть вызвана только локально.

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)    #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

В определении функции также поддерживаются охранные условия и множественные варианты аргументов. Эликсир будет проверять каждый вариант, пока не найдёт подходящий. Вот реализация функции, которая проверяет, является ли переданное ей число нулём:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_integer(x) do
    false
  end
end

IO.puts Math.zero?(0)         #=> true
IO.puts Math.zero?(1)         #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0)       #=> ** (FunctionClauseError)

Передача аргумента, который не подходит ни одному из вариантов, вызовет ошибку.

Аналогично конструкциям, вроде if, именованные функции поддерживают как синтаксис блоков do:, так и do/end, который мы изучили вместе с ключевыми списками. Например, мы можем привести math.exs к следующему виду:

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_integer(x), do: false
end

Такой вариант даст нам аналогичное поведение. Вы можете использовать do: для записи в одну строку, но для многострочного кода всегда нужны do/end.

Отлов функций

На протяжении этого руководства мы использовали нотацию имя_функции/арность для обозначений функций. Такая нотация также может быть использована для получения именованной функции в качестве типа «функция». Запустите iex, открыв описанный выше файл math.exs:

$ iex math.exs
iex> Math.zero?(0)
true

iex> fun = &Math.zero?/1
&Math.zero?/1

iex> is_function(fun)
true

iex> fun.(0)
true

Помните, что в Эликсире есть разница между анонимными и именованными функциями. В первых для вызова должна быть указана точка (.) между именем переменной и скобками. Оператор захвата & позволяет именованным функциям быть привязанным к переменным и переданным в качестве арумента так же, как мы привязываем, исполняем и передаём анонимные функции.

Локальные или импортированные функции, такие как is_function/1, могут быть использованы без модуля:

iex> &is_function/1
&:erlang.is_function/1

iex> (&is_function/1).(fun)
true

Обратите внимание, что синтаксис захвата может быть также использован в качестве сокращения для создания функций:

iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>

iex> fun.(1)
2

&1 воспринимается как первый аргумент, переданный в функцию. &(&1+1) выше, то же самое, что fn x -> x + 1 end. Синтаксис выше полезен для объявления коротких функций.

Если вы хотите захватить функцию из модуля, вы можете использовать &Module.function():

iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]

&List.flatten(&1, &2) аналогично fn(list, tail) -> List.flatten(list, tail) end, а это в данном случае равноценно &List.flatten/2. Вы можете получить больше информации об операторе & в документации по Kernel.SpecialForms.

Стандартные значения аргументов

Именованные функции в Эликсире также поддерживают стандартные значения аргументов:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

Любое выражение может быть принято в качестве стандартного значения, но при описании функции оно не будет оценено. При каждом вызове функции, когда нужно использовать стандартное значение, оно будет подставляться во время исполнения:

defmodule DefaultTest do
  def dowork(x \\ "hello") do
    x
  end
end
iex> DefaultTest.dowork
"hello"
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
"hello"

Если функция со стандартным значением имеет несколько вариантов исполнения, нужно сначала объявить функцию без её описания (название и список аргументов без тела функции), чтобы задать значения по умолчанию:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

При использовании стандартных значений следует быть осторожным во избежание перекрытия описаний функции. Рассмотрите следующий пример:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

Если мы сохраним код выше в файл с названием concat.ex и скомпилируем его, Эликсир покажет следующее предупреждение:

warning: this clause cannot match because a previous clause at line 2 always matches

Компилятор говорит нам, что вызов функции join с двумя аргументами всегда выберет первое определение, а второе будет выполнено только с тремя переданными аргументами:

$ iex concat.exs
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

На этом мы закончим наше краткое введение в модули. В следующих главах мы изучим, как использовать именованные функции для рекурсии, рассмотрим лексические директивы Эликсира, которые можно использовать для импорта функций из других модулей и поговорим об атрибутах модулей.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.