Source Атрибуты модуля
Атрибуты модуля в Elixir служат трём целям:
- в качестве аннотаций модуля и функций
- в качестве временного хранилища данных модуля, используемого во время компиляции
- в качестве констант, вычисляемых на этапе компиляции
Давайте рассмотрим эти примеры.
В качестве аннотаций
Elixir позаимствовал концепцию атрибутов модулей из Erlang. Например:
defmodule MyServer do @moduledoc "My server code." end
В приведенном выше примере мы определяем документацию модуля, используя синтаксис атрибута модуля. В Elixir есть ряд зарезервированных атрибутов. Вот некоторые из них, наиболее часто используемые:
-
@moduledoc— предоставляет документацию для текущего модуля. -
@doc— предоставляет документацию для функции или макроса, следующего за атрибутом. -
@spec— предоставляет тип для функции, следующей за атрибутом. -
@behaviour— (обратите внимание на британское написание) используется для указания поведения OTP или определённого пользователем.
@moduledoc и @doc являются, пожалуй, наиболее часто используемыми атрибутами, и мы ожидаем, что вы будете их часто применять. Elixir рассматривает документацию как первостепенный элемент и предоставляет множество функций для доступа к документации. Мы рассмотрим их в отдельной главе.
Вернёмся к модулю Math из предыдущих глав, добавим к нему документацию и сохраним в файл math.ex:
defmodule Math do
@moduledoc """
Provides math-related functions.
## Examples
iex> Math.sum(1, 2)
3
"""
@doc """
Calculates the sum of two numbers.
"""
def sum(a, b), do: a + b
end
Elixir рекомендует использовать Markdown с heredocs для написания удобочитаемой документации. Heredocs — это многострочные строки, они начинаются и заканчиваются тройными двойными кавычками, сохраняя форматирование внутреннего текста. Мы можем получить доступ к документации любого скомпилированного модуля напрямую из IEx:
$ elixirc math.ex $ iex
iex> h Math # Access the docs for the module Math ... iex> h Math.sum # Access the docs for the sum function ...
Мы также предоставляем инструмент под названием ExDoc, который используется для генерации HTML-страниц из документации.
Вы можете ознакомиться с документацией для Module, чтобы получить полный список поддерживаемых атрибутов. Elixir также использует атрибуты для аннотирования нашего кода типами.
В качестве временного хранилища
До сих пор мы видели, как определять атрибуты, но как их можно прочитать? Давайте посмотрим на пример:
defmodule MyServer do
@service URI.parse("https://example.com")
IO.inspect(@service)
end
Новые строки
Не добавляйте новую строку между атрибутом и его значением, иначе Elixir будет считать, что вы читаете значение, а не устанавливаете его.
Попытка получить доступ к атрибуту, который не был определён, выведет предупреждение:
defmodule MyServer do @unknown end warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access
Атрибуты также можно читать внутри функций:
defmodule MyApp.Status do
@service URI.parse("https://example.com")
def status(email) do
SomeHttpClient.get(@service)
end
end
Атрибут модуля определяется во время компиляции, и его возвращаемое значение, а не сам вызов функции, подставляется вместо атрибута. Таким образом, вышеприведённый код фактически будет скомпилирован следующим образом:
defmodule MyApp.Status do
def status(email) do
SomeHttpClient.get(%URI{
authority: "example.com",
host: "example.com",
port: 443,
scheme: "https"
})
end
end
Это может быть полезно для предварительного вычисления значений и последующей их инъекции в модуль. Именно это мы имеем в виду под временным хранилищем: после компиляции модуля атрибут модуля удаляется, за исключением функций, которые считывали атрибут. Обратите внимание, что вы не можете вызывать функции, определённые в том же модуле, в качестве части самого атрибута, так как эти функции ещё не определены.
Каждый раз, когда мы считываем атрибут внутри функции, Elixir создаёт снимок его текущего значения. Поэтому, если вы читаете один и тот же атрибут несколько раз внутри нескольких функций, вы увеличиваете время компиляции, так как Elixir теперь должен компилировать каждый снимок. Как правило, вы должны избегать многократного считывания одного и того же атрибута и вместо этого переместить его в функцию. Например, вместо этого:
def some_function, do: do_something_with(@example) def another_function, do: do_something_else_with(@example)
Предпочтительнее следующее:
def some_function, do: do_something_with(example()) def another_function, do: do_something_else_with(example()) defp example, do: @example
В качестве констант, вычисляемых на этапе компиляции
Атрибуты модуля также могут быть полезны в качестве констант, вычисляемых на этапе компиляции. Как правило, сами функции достаточно для роли констант в кодовой базе. Например, вместо определения:
@hours_in_a_day 24
Вы должны предпочесть:
defp hours_in_a_day(), do: 24
Вы можете даже определить публичную функцию, если она должна использоваться в разных модулях. Во многих проектах есть модуль под названием MyApp.Constants, который определяет все константы, используемые во всей кодовой базе.
Вы можете даже иметь сложные структуры данных в качестве констант, если они состоят только из других типов данных (без вызовов функций, операторов и других выражений). Например, вы можете указать константу конфигурации системы следующим образом:
defp system_config(), do: %{timezone: "Etc/UTC", locale: "pt-BR"}
Поскольку структуры данных в Elixir неизменяемы, выделяется только один экземпляр структуры данных выше, который используется во всех вызовах функций, при условии, что он не содержит каких-либо исполняемых выражений.
Случаи использования атрибутов модуля возникают, когда вам нужно выполнить некоторую работу на этапе компиляции и затем встроить её результаты внутрь функции. Распространённым случаем является использование атрибутов модулей в шаблонах и условиях (в качестве альтернативы defguard/1), так как они поддерживают ограниченный набор выражений:
# Inside pattern @default_timezone "Etc/UTC" def shift(@default_timezone), do: ... # Inside guards @time_periods [:am, :pm] def shift(time, period) when period in @time_periods, do: ...
Атрибуты модуля как константы и как временное хранилище чаще всего используются вместе: атрибут модуля используется для вычисления и хранения дорогостоящего значения, а затем экспонируется как константа из этого модуля.
Дальнейшее изучение
Библиотеки и фреймворки могут использовать атрибуты модулей для предоставления пользовательских аннотаций. Для примера обратитесь к фреймворку для тестирования юнит-тестов Elixir под названием ExUnit. ExUnit использует атрибуты модулей для разных целей:
defmodule MyTest do
use ExUnit.Case, async: true
@tag :external
@tag os: :unix
test "contacts external service" do
# ...
end
end
В приведённом выше примере ExUnit хранит значение async: true в атрибуте модуля, чтобы изменить способ компиляции модуля. Теги также работают как аннотации, и их можно предоставлять несколько раз благодаря способности Elixir накапливать атрибуты. Затем вы можете использовать теги для настройки и фильтрации тестов, например, чтобы избежать выполнения тестов, специфичных для Unix, при запуске вашей тестовой среды на Windows.
Чтобы полностью понять, как работает ExUnit, нам понадобятся макросы, поэтому мы вернёмся к этому шаблону в руководстве по метапрограммированию и узнаем, как использовать атрибуты модулей в качестве хранилища для пользовательских аннотаций.
В следующих главах мы изучим структуры и протоколы, прежде чем перейти к обработке исключений и другим конструкциям, таким как сигилы и выражения.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/module-attributes.html