[Open source, Erlang/OTP, Elixir/Phoenix] Типы в рантайме: глубже в крольчью нору
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Когда я начинал писать заметку «Типы, где их не ждали», мне казалось, что я осилил принести эрланговские типы в рантайм и теперь могу их использовать в клиентском коде на эликсире. Ха-ха, как же я был наивен.
Все, что предложено по ссылке, будет работать для явных определений типа по месту использования, наподобие use Foo, var: type(). К сожалению, такой подход обречен, если мы хотим определить типы где-нибудь в другом месте: рядом в коде при помощи атрибутов модуля, или, там, в конфиге. Например, для определения структуры мы можем захотеть написать что-то типа такого:
# @fields [foo: 42]
# defstruct @fields
@definition var: atom()
use Foo, @definition
Код выше не то, что не обработает тип так, как нам хочется — он не соберется вовсе, потому что @definition var: atom() выбросит исключение ** (CompileError) undefined function atom/0.
Наивный подход
Один из моих самых любимых профессиональных афоризмов — «недели кодирования могут сэкономить вам часы планирования» (обычно приписывается @tsilb, но пользователь был заблокирован супердемократичным твиттером, поэтому я не уверен.) Мне она так нравится, что я повторяю ее как мантру на каждом втором совещании, но, как это всегда бывает с незыблемыми жизненными принципами, — часто не следую провозглашаемому сам.
Итак, я начал с того, что сделал две разных реализации __using__/1: одну, которая принимает список (и ожидает увидеть в нем пары field → type()), и другую — принимающую все, что угодно, ожидая встретить в аргументах либо квотированные типы, либо триплы {Module, :type, [params]}. Я использовал сигил ~q||, который был услужливо имплементирован мной же, в одном из стародавних игрушечных проектов, во времена, когда я учился работать с макросами и AST. Он позволяет вместо quote/1 писать лаконичнее: foo: ~q|atom()|. Там внутри я руками строил список, который потом передавался в первую функцию, принимающую списки. Весь этот код был настоящим кошмаром. Я сомневаюсь, что видел что-то более невнятное за всю свою карьеру, несмотря на то, что я чувствую себя абсолютно комфортно с регулярными выражениями, они мне нравятся, и я их часто использую. Однажды я выиграл спор на воспроизведение регулярного выражения для электронной почты максимально близко к оригиналу, но этот код, всего-то передававший туда-сюда старый добрый простой эрланговский тип оказался в пять раз запутаннее и как-то неаккуратнее, что ли.
Все это выглядело безумием. Мне на подсознательном уровне кажется, что работа с нативными типами erlang во время выполнения — не должна нуждаться в тонне переусложненного избыточного кода, который весь выглядел как заклинание, которое может вызвать духов Кобола. Поэтому я заварил себе кофе покрепче — и начал думать вместо того, чтобы писать код, который никто никогда не сможет понять позже (включая меня самого).
Вот ссылка на работающую версию, для истории и в назидание. Я далек от того, чтобы гордиться этим кодом, но я уверен, что мы должны делиться всеми ошибками, которые сделали в прошлом, свернув не туда, — а не только историями успеха. Все эти побасенки всегда более вдохновляющие и волнующие, чем сухой пересказ конечного результата.
Tyyppi
Пару дней спустя, я вышел из дома искупаться — и на пляже вдруг понял, что я столкнулся с типичной XY проблемой. Все, что мне было нужно, — так это просто сделать эрланговские типы — почетным гражданином рантайме. Так родилась библиотека Tyyppi.
В ядре эликсира присутствует незадокументированный модуль Code.Typespec, который существенно облегчил мне жизнь. Я начал с очень простого подхода: с проверки всех возможных термов по всем возможным типам. Я просто загрузил все типы, доступные в моей текущей сессии, и дописывал новые обработчики по мере того, как рекурсивный анализ типов падал глубже по рекурсии. Честно говоря, это было скорее скучно, чем весело. Зато оно привело меня к первой полезной части этой библиотеки — функции Tyyppi.of?/2, которая принимает тип и терм, а возвращает — логическое значение «да»/«нет» в зависимости от того, принадлежит ли терм указанному типу.
iex|tyyppi|1 Tyyppi.of? GenServer.on_start(), {:ok, self()}
#⇒ true
iex|tyyppi|2 Tyyppi.of? GenServer.on_start(), :ok
#⇒ false
Мне нужно было какое-то внутреннее представление для типов, поэтому я решил хранить все в виде структуры с именем Tyyppi.T. Так у Tyyppi.of?/2 появился брат-близнец — Tyyppi.of_type?/2.
iex|tyyppi|3 type = Tyyppi.parse(GenServer.on_start)
iex|tyyppi|4 Tyyppi.of_type? type, {:ok, self()}
#⇒ true
Единственный нюанс, связанный с этим подходом, заключается в том, что мне нужно загрузить и сохранить все типы, доступные в системе, и эта информация не будет доступна в релизах. На данный момент я прекрасно справляюсь с хранением всего этого в обычном файле при помощи :erlang.term_to_binary/1, который связывается с релизом и загружается через обычный специализированный Config.Provider.
Структуры
Теперь я был полностью вооружен, чтобы вернуться к своей первоначальной задаче: создать удобный способ объявления типизированной структуры. Со всем этим багажом на борту, это было легко. Я решил ограничить само объявление структуры явным встроенным литералом, содержащим пары key: type(). Также я реализовал для него Access, с проверкой типов при upserts. Имея все это под рукой, я решил позаимствовать еще пару идей у Ecto.Changeset и добавил перегружаемые функции cast_field/1 и validate/1.
Теперь мы можем объявить структуру, которая разрешала бы апсерты, тогда и только тогда, когда типы всех значений верны, (и когда проходит пользовательская проверка, если была задана).
defmodule MyStruct do
import Kernel, except: [defstruct: 1]
import Tyyppi.Struct, only: [defstruct: 1]
@typedoc "The user type defined before `defstruct/1` declaration"
@type my_type :: :ok | {:error, term()}
@defaults foo: :default,
bar: :erlang.list_to_pid('<0.0.0>'),
baz: {:error, :reason}
defstruct foo: atom(), bar: GenServer.on_start(), baz: my_type()
def cast_foo(atom) when is_atom(atom), do: atom
def cast_foo(binary) when is_binary(binary),
do: String.to_atom(binary)
def validate(%{foo: :default} = my_struct), do: {:ok, my_struct}
def validate(%{foo: foo} = my_struct), do: {:error, {:foo, foo}
end
Я понятия не имею, какова практическая ценность этой библиотеки в продакшене (вру, знаю я: никакая), но она, безусловно, может быть отличным помощником во время разработки, позволяя сузить круг поиска и вычленить странные ошибки, связанные с динамической природой типов в Elixir, особенно при работе с внешними источниками.
Весь код библиотеки доступен, как всегда, на гитхабе.
Удачного рантаймтайпинга!
===========
Источник:
habr.com
===========
Похожие новости:
- [Децентрализованные сети, Open source, Научно-популярное, Криптовалюты] Протоколы, а не Платформы: технологический подход к свободе слова — Часть 1 (перевод)
- [Программирование, Машинное обучение, Искусственный интеллект, Голосовые интерфейсы] Open Source синтез речи SOVA
- [Open source, Машинное обучение, Искусственный интеллект] DeepMind открыла код среды Lab2D для обучения нейросетей
- [Информационная безопасность, Open source, Администрирование баз данных] Сканер для выявления слабых паролей в СУБД
- [Open source, GitHub, Законодательство в IT] GitHub учредил фонд на $1 млн для помощи разработчикам в борьбе с неправомерными запросами на удаление
- [Open source, GitHub, Законодательство в IT, История IT, IT-компании] youtube-dl вернулся в GitHub, спорный контент в readme удалён
- [Open source, Биографии гиков] В память об Аароне Шварце провели виртуальный хакатон
- [Open source] Как монетитзируется Open-Source
- [Open source, *nix] FOSS News №42 – дайджест новостей и других материалов о свободном и открытом ПО за 9-15 ноября 2020 года
- [Open source, Системы управления версиями] Установка и настройка cSvn
Теги для поиска: #_open_source, #_erlang/otp, #_elixir/phoenix, #_macros, #_macro, #_injection, #_open_source, #_erlang/otp, #_elixir/phoenix
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:31
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Когда я начинал писать заметку «Типы, где их не ждали», мне казалось, что я осилил принести эрланговские типы в рантайм и теперь могу их использовать в клиентском коде на эликсире. Ха-ха, как же я был наивен. Все, что предложено по ссылке, будет работать для явных определений типа по месту использования, наподобие use Foo, var: type(). К сожалению, такой подход обречен, если мы хотим определить типы где-нибудь в другом месте: рядом в коде при помощи атрибутов модуля, или, там, в конфиге. Например, для определения структуры мы можем захотеть написать что-то типа такого: # @fields [foo: 42]
# defstruct @fields @definition var: atom() use Foo, @definition Код выше не то, что не обработает тип так, как нам хочется — он не соберется вовсе, потому что @definition var: atom() выбросит исключение ** (CompileError) undefined function atom/0. Наивный подход Один из моих самых любимых профессиональных афоризмов — «недели кодирования могут сэкономить вам часы планирования» (обычно приписывается @tsilb, но пользователь был заблокирован супердемократичным твиттером, поэтому я не уверен.) Мне она так нравится, что я повторяю ее как мантру на каждом втором совещании, но, как это всегда бывает с незыблемыми жизненными принципами, — часто не следую провозглашаемому сам. Итак, я начал с того, что сделал две разных реализации __using__/1: одну, которая принимает список (и ожидает увидеть в нем пары field → type()), и другую — принимающую все, что угодно, ожидая встретить в аргументах либо квотированные типы, либо триплы {Module, :type, [params]}. Я использовал сигил ~q||, который был услужливо имплементирован мной же, в одном из стародавних игрушечных проектов, во времена, когда я учился работать с макросами и AST. Он позволяет вместо quote/1 писать лаконичнее: foo: ~q|atom()|. Там внутри я руками строил список, который потом передавался в первую функцию, принимающую списки. Весь этот код был настоящим кошмаром. Я сомневаюсь, что видел что-то более невнятное за всю свою карьеру, несмотря на то, что я чувствую себя абсолютно комфортно с регулярными выражениями, они мне нравятся, и я их часто использую. Однажды я выиграл спор на воспроизведение регулярного выражения для электронной почты максимально близко к оригиналу, но этот код, всего-то передававший туда-сюда старый добрый простой эрланговский тип оказался в пять раз запутаннее и как-то неаккуратнее, что ли. Все это выглядело безумием. Мне на подсознательном уровне кажется, что работа с нативными типами erlang во время выполнения — не должна нуждаться в тонне переусложненного избыточного кода, который весь выглядел как заклинание, которое может вызвать духов Кобола. Поэтому я заварил себе кофе покрепче — и начал думать вместо того, чтобы писать код, который никто никогда не сможет понять позже (включая меня самого). Вот ссылка на работающую версию, для истории и в назидание. Я далек от того, чтобы гордиться этим кодом, но я уверен, что мы должны делиться всеми ошибками, которые сделали в прошлом, свернув не туда, — а не только историями успеха. Все эти побасенки всегда более вдохновляющие и волнующие, чем сухой пересказ конечного результата. Tyyppi Пару дней спустя, я вышел из дома искупаться — и на пляже вдруг понял, что я столкнулся с типичной XY проблемой. Все, что мне было нужно, — так это просто сделать эрланговские типы — почетным гражданином рантайме. Так родилась библиотека Tyyppi. В ядре эликсира присутствует незадокументированный модуль Code.Typespec, который существенно облегчил мне жизнь. Я начал с очень простого подхода: с проверки всех возможных термов по всем возможным типам. Я просто загрузил все типы, доступные в моей текущей сессии, и дописывал новые обработчики по мере того, как рекурсивный анализ типов падал глубже по рекурсии. Честно говоря, это было скорее скучно, чем весело. Зато оно привело меня к первой полезной части этой библиотеки — функции Tyyppi.of?/2, которая принимает тип и терм, а возвращает — логическое значение «да»/«нет» в зависимости от того, принадлежит ли терм указанному типу. iex|tyyppi|1 Tyyppi.of? GenServer.on_start(), {:ok, self()}
#⇒ true iex|tyyppi|2 Tyyppi.of? GenServer.on_start(), :ok #⇒ false Мне нужно было какое-то внутреннее представление для типов, поэтому я решил хранить все в виде структуры с именем Tyyppi.T. Так у Tyyppi.of?/2 появился брат-близнец — Tyyppi.of_type?/2. iex|tyyppi|3 type = Tyyppi.parse(GenServer.on_start)
iex|tyyppi|4 Tyyppi.of_type? type, {:ok, self()} #⇒ true Единственный нюанс, связанный с этим подходом, заключается в том, что мне нужно загрузить и сохранить все типы, доступные в системе, и эта информация не будет доступна в релизах. На данный момент я прекрасно справляюсь с хранением всего этого в обычном файле при помощи :erlang.term_to_binary/1, который связывается с релизом и загружается через обычный специализированный Config.Provider. Структуры Теперь я был полностью вооружен, чтобы вернуться к своей первоначальной задаче: создать удобный способ объявления типизированной структуры. Со всем этим багажом на борту, это было легко. Я решил ограничить само объявление структуры явным встроенным литералом, содержащим пары key: type(). Также я реализовал для него Access, с проверкой типов при upserts. Имея все это под рукой, я решил позаимствовать еще пару идей у Ecto.Changeset и добавил перегружаемые функции cast_field/1 и validate/1. Теперь мы можем объявить структуру, которая разрешала бы апсерты, тогда и только тогда, когда типы всех значений верны, (и когда проходит пользовательская проверка, если была задана). defmodule MyStruct do
import Kernel, except: [defstruct: 1] import Tyyppi.Struct, only: [defstruct: 1] @typedoc "The user type defined before `defstruct/1` declaration" @type my_type :: :ok | {:error, term()} @defaults foo: :default, bar: :erlang.list_to_pid('<0.0.0>'), baz: {:error, :reason} defstruct foo: atom(), bar: GenServer.on_start(), baz: my_type() def cast_foo(atom) when is_atom(atom), do: atom def cast_foo(binary) when is_binary(binary), do: String.to_atom(binary) def validate(%{foo: :default} = my_struct), do: {:ok, my_struct} def validate(%{foo: foo} = my_struct), do: {:error, {:foo, foo} end Я понятия не имею, какова практическая ценность этой библиотеки в продакшене (вру, знаю я: никакая), но она, безусловно, может быть отличным помощником во время разработки, позволяя сузить круг поиска и вычленить странные ошибки, связанные с динамической природой типов в Elixir, особенно при работе с внешними источниками. Весь код библиотеки доступен, как всегда, на гитхабе. Удачного рантаймтайпинга! =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:31
Часовой пояс: UTC + 5