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

Модули

Мы все знаем из опыта, насколько неудобно хранить все функции в одном файле и одной области видимости. В этом уроке мы разберемся как группировать функции и определять специальный ассоциативный массив, известный как struct, для более эффективной организации кода.

Модули

Модули позволяют организовывать функции по областям видимости. Кроме группировки функций, они позволяют определять именованные и закрытые функции, которые мы рассмотрели в уроке о функциях.

Давайте рассмотрим простой пример:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

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

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

Атрибуты модулей

Атрибуты модулей зачастую используются в языке как константы. Давайте рассмотрим простой пример:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

Важно помнить, что есть несколько зарезервированных названий атрибутов в Elixir. Три самые часто используемые:

Структуры

Структура - специальный ассоциативный массив с определенным набором ключей и их значениями по умолчанию. Она должна быть определена в модуле, из которого и берет свое имя. Часто структура - это единственная вещь, определенная в модуле.

Для определения структуры мы используем конструкцию defstruct вместе с ключевым списком полей и значений по умолчанию:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

Давайте создадим несколько структур:

iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>

iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>

iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>

Обновление структуры работает так же, как и обновление ассоциативного массива:

iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>

Что еще более важно, структуры можно сопоставлять с ассоциативными массивами:

iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>

Начиная с Elixir 1.8 структуры включают в себя пользовательскую интроспекцию. Чтобы понять, что она значит и как следует её использовать, давайте выведем значение переменной sean:

iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"

В выводе видны все наши поля, что, конечно, хорошо для примера, но что если у нас есть защищённое поле, которое мы не хотим показывать? Новая директива @derive позволяет сделать это! Давайте обновим пример, чтобы ключ roles больше не включался в вывод:

defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end

Примечание: также можно использовать @derive {Inspect, except: [:roles]} — это то же самое, что и выше.

После обновления модуля давайте посмотрим, что сейчас будет происходить в iex:

iex> sean = %Example.User<name: "Sean", roles: [...], ...>
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"

Ключ roles больше не показывается в выводе!

Композиция

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

alias

Позволяет использовать сокращенное именование модулей, используется довольно часто:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# Без alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

Если есть конфликт между двумя псевдонимами или нужно сделать псевдоним на произвольное имя - можно использовать опцию :as:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

Также можно использовать эту возможность для нескольких модулей за раз:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

import

Если нужно импортировать функции, вместо создания псевдонима модуля можно использовать import:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

Фильтрация

По умолчанию импортируются все функции и макросы, но можно сократить это количество с использованием опций :only и :except.

Для импорта определенных функций и макросов, мы должны предоставить пары имя/количество аргументов в :only и :except. Давайте начнем с импорта функции last/1:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

Если импортировать все, кроме last/1, и попробовать тот же код, что и раньше:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

Вдобавок к указанию определенной сигнатуры функции, существует два специальных атома - :functions и :macros, которые импортируют только функции и макросы соответственно:

import List, only: :functions
import List, only: :macros

require

Чтобы сообщить Elixir, что мы собираемся использовать макросы из другого модуля, можно использовать require. Небольшое отличие от import заключается в том, что require позволяет использовать только макросы указанного модуля, но не функции.

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

Если же мы попробуем обратиться к макросу, который еще не загружен, Elixir выдаст ошибку.

use

С помощью макроса use мы можем использовать другой модуль, чтобы изменить определение нашего текущего модуля. Когда мы вызываем use в коде, на самом деле мы обращаемся к функции обратного вызова __using__/1, объявленной в указанном модуле. Результат выполнения макроса __using__/1 становится частью определения нашего модуля. Чтобы получше разобраться, как это работает, взглянем на простой пример:

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

Здесь мы создали модуль Hello, определяющий функцию обратного вызова __using__/1, внутри которой объявлена функция hello/1. Создадим ещё один модуль, чтобы попробовать наш новый код:

defmodule Example do
  use Hello
end

Запустив наш код в IEx, мы увидим, что функция hello/1 доступна из модуля Example:

iex> Example.hello("Sean")
"Hi, Sean"

Итак, мы видим, что use вызвала функцию обратного вызова __using__/1 в Hello, а та, в свою очередь, добавила итоговый код в наш модуль. Теперь, когда мы разобрали простой пример, обновим код, чтобы посмотреть, как __using__/1 поддерживает опции. Сделаем это, добавив опцию greeting:

defmodule Hello do
  defmacro __using__(opts) do
    greeting = Keyword.get(opts, :greeting, "Hi")

    quote do
      def hello(name), do: unquote(greeting) <> ", " <> name
    end
  end
end

Также обновим модуль Example, включив в него свежесозданную опцию greeting:

defmodule Example do
  use Hello, greeting: "Hola"
end

Запустив это в IEx, мы увидим, что приветствие изменилось:

iex> Example.hello("Sean")
"Hola, Sean"

Эти примеры были простыми, чтобы просто продемонстрировать, как работает use, однако это невероятно сильное средство из набора инструментов Elixir. Продолжая изучать Elixir, обращайте внимание на use. Один пример, который точно попадётся на пути — это use ExUnit.Case, async: true.

Примечание: макросы quote, alias, use, require используются при работе с метапрограммированием.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!