Модули
Мы все знаем из опыта, насколько неудобно хранить все функции в одном файле и одной области видимости.
В этом уроке мы разберемся как группировать функции и определять специальный ассоциативный массив, известный как 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. Три самые часто используемые:
-
moduledoc
— Документирует текущий модуль. -
doc
— Документация функций и макросов. -
behaviour
— Использование OTP или определенного пользователем поведения.
Структуры
Структура - специальный ассоциативный массив с определенным набором ключей и их значениями по умолчанию. Она должна быть определена в модуле, из которого и берет свое имя. Часто структура - это единственная вещь, определенная в модуле.
Для определения структуры мы используем конструкцию 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!