--- version: 1.0.1 title: Cпецификации и типы --- В этом уроке мы узнаем о директивах `@spec` и `@type`. Первая — скорее дополнение синтаксиса языка для написания документации, которая может быть проанализирована специальными инструментами. Вторая — помогает нам писать более читаемый и понятный код. {% include toc.html %} ## Введение Иногда вы можете захотеть сделать описание для интерфейса вашей функции. Конечно, вы можете использовать [аннотацию @doc](../../basics/documentation), но это информация для других разработчиков, которая не проверяется во время компиляции. В Elixir есть аннотация `@spec`, она позволяет описать спецификацию функции, и может использоваться для дополнительной проверки кода. Такая спецификация может получиться весьма объёмной из-за сложных параметров. Вы можете упростить её, определяя свои типы. В Elixir для этого существует аннотация `@type`. С другой стороны, Elixir всё равно остаётся динамическим языком. Это значит, что вся информация о типах будет проигнорирована компилятором, но может быть использована другими инструментами. ## Спецификация Если у вас есть опыт работы с Java или Ruby, вы можете думать о спецификации как об интерфейсе. Спецификация определяет типы параметров и возвращаемого значения функции. Чтобы описать принимаемые и возращаемые функцией типы мы используем директиву `@spec`, распологая её прямо перед определением функции. Директива принимает в качестве параметров: имя функции, список типов аргументов функции, и, после знака `::`, тип возвращаемого значения. Рассмотрим пример: ```elixir @spec sum_product(integer) :: integer def sum_product(a) do [1, 2, 3] |> Enum.map(fn el -> el * a end) |> Enum.sum end ``` Всё выглядит верно и мы получим правильный результат после вызова функции, но `Enum.sum` возвращает значение типа `number` а не `integer`, как мы ожидали в `@spec`. Это может быть источником ошибок! Существуют инструменты статического анализа кода, например `Dialyzer`, которые помогают нам найти такие ошибки. Мы поговорим о них в других уроках. ## Пользовательские типы Создание спецификации — хорошо, но иногда наши функции работают со сложными структурами данных, вместо чисел или списков. В таком случае определение в `@spec` будет очень сложно понять и/или изменить. Иногда функциям необходимо принимать большое число параметров или возвращать сложные структуры данных. Длинный список параметров — это одно из потенциально проблемных мест кода. В объектно-ориентированных языках, таких как Ruby или Java, мы легко можем определить классы, которые помогут нам решить эту проблему. В Elixir нет классов, но благодаря тому, что он легко расширяем, мы можем определять наши собственные типы. Изначально Elixir уже содержит некоторые простые типы, такие как `integer` или `pid`. Вы можете найти полный список доступных типов в [документации](https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax). ### Определение пользовательского типа Изменим нашу функцию `sum_times` и добавим несколько дополнительных параметров: ```elixir @spec sum_times(integer, %Examples{first: integer, last: integer}) :: integer def sum_times(a, params) do for i <- params.first..params.last do i end |> Enum.map(fn el -> el * a end) |> Enum.sum |> round end ``` Мы добавили в модуль `Examples` струкутру, которая содержит два поля: `first` и `last`. Это упрощенная версия структуры из модуля `Range`. Мы поговорим о `структурах` в разделе [модули](../../basics/modules/#structs). Представьте, что нам надо написать в нашем коде спецификации, включающие структуру `Examples`, множество раз. Это будет довольно нудно — писать длинную, сложную спецификацию, и может послужить источником ошибок. Решением этой проблемы будет директива `@type`. У `Elixir` есть три директивы для определения типов: - `@type` — простой публичный тип. Внутренняя структура типа тоже публична. - `@typep` — тип закрытый и может использоваться только в том модуле, в котором описан. - `@opaque` — тип публичный, но его внутренняя структура закрыта. Запишем определение нашего типа: ```elixir defmodule Examples do defstruct first: nil, last: nil @type t(first, last) :: %Examples{first: first, last: last} @type t :: %Examples{first: integer, last: integer} end ``` Мы определили тип `t(first, last)`, который представляет нашу структуру `%Examples{first: first, last: last}`. Теперь мы видим, что типы могут принимать параметры. Но мы также опредили и тип `t`, в данном случае это представление структуры `%Examples{first: integer, last: integer}`. В чем отличие? Первый — представляет структуру `Examples`, в которой оба ключа могут иметь любой тип. Второй — представляет структуру, ключи в которой имеют тип `integer`. Это означает, что такой код: ```elixir @spec sum_times(integer, Examples.t) :: integer def sum_times(a, params) do for i <- params.first..params.last do i end |> Enum.map(fn el -> el * a end) |> Enum.sum |> round end ``` равносилен коду: ```elixir @spec sum_times(integer, Examples.t(integer, integer)) :: integer def sum_times(a, params) do for i <- params.first..params.last do i end |> Enum.map(fn el -> el * a end) |> Enum.sum |> round end ``` ### Документирование типов Последний вопрос, который нам надо обсудить: как документировать наши типы. Как известно из урока [документирование](../../basics/documentation), для описания функций и модулей у нас есть аннотации `@doc` и `@moduledoc`. Для описания наших типов мы можем использовать `@typedoc`: ```elixir defmodule Examples do @typedoc """ Тип, который представляет структуру Examples с полями :first типа integer и :last типа integer. """ @type t :: %Examples{first: integer, last: integer} end ``` Директива `@typedoc` аналогична директивам `@doc` и `@moduledoc`.