Эликсир позаимствовал из Эрланга многое. Например, оба — языки с динамической
типизацией (что прекрасно, что бы вам там категоричные строгие типы в касках
не говорили). При этом, в обоих языках присутствуют расширенные возможности для
проверки типов, когда это надо.  Вот введение в typespecs, а вот здесь можно ознакомиться с ними подробнее. Если вкратце, мы можем определить спецификацию сигнатуры функции, и если вызов не соответствует объявленным ожиданиям, статический анализатор кода, известный как dialyzer, заругается. Формат этих спецификаций выглядит довольно изящно: Эта фунция ожидает на входе строку и любой терм, и возвращает строку (получившуюся конкатенацией первого параметра и приведенного к строковому типу второго). Когда мы решили поддержать явное указание типов для методов в Dry::Protocols, я поэкспериментировал немного с синтаксом. Мне удалось почти полностью повторить эликсировские typespec: Вот эта вот инструкция @spec парсится и исполняется стандартным парсером ruby. Хочу рассказать, как она была реализована. Если вы думаете, что умеете в ruby, очень рекомендую не читать дальше, а попробовать добиться такого же результата — это весело. Да, я в курсе про contracts.ruby, но мне совсем не хотелось тащить такого невнятного монстра в малюсенькую прикладную библиотеку, ну и я уже давно не доверяю коду из интернетов. Ну, чтобы сделать задачу поинтереснее, я намеренно решил добиваться синтаксиса, максимально похожего на оригинальный. Я, разумеется, мог пойти по скучному монорельсовому пути и объявить многословный, занудный и раздражающий DSL, но я не такой деревянный кодер. Итак, поглядим, что нам позволит стандартный парсер ruby. Одинокая instance-переменная будет просто проигнорирована (ну, для пуристов: значение nil будет возвращено на стадии парсинга и мгновенно позабыто), поэтому у нас остается примерно три варианта: присвоить ей значение типа, или вызвать напрямую. Мне лично больше глянулся вариант с вызовом @spec[...] (который просто делегирует @spec.call под капотом). Теперь параметры. Простейший способ скормить парсеру кучу всего, чего угодно — создать инстанс какого-нибудь всеядного класса-аккумулятора и возвращать self из каждого вызова method_missing. Чтобы по максимуму избежать наложений имен, я унаследуюсь от BasicObject, а не от стандартного отца-родителя Object: Я такие дикие имена даю методам этого класса не случайно: не хотелось бы, чтобы наследник случайно его переписал. Да, этот подход довольно опасен в принципе, потому что мы сейчас перепишем method_missing в модуле Annotation, который потом будет включать везде, где нам будут нужны аннотации. Ну, это же просто демонстрация мощи ruby, так что нормально. И, кстати, для исходной задачи аннотирования методов в Dry::Protocols, это почти безопасно: протоколы в принципе очень изолированы и определяют всего несколько стоящих особняком методов: такой дизайн. Ну, поехали. У нас уже есть все, чтобы поддержать синтак аннотаций вида @spec[foo, bar, baz]. Пора включить Annotation в какой-нибудь класс и посмотреть, что получится. А, ну да, BasicObject же. Определим его: Voilà. Оно уже как-то работает. В смысле, оно не ругается на синтаксические ошибки и не сбивает с толку парсер и интерпретатор. Ну, начинается интересное. Хочется не ограничиваться одним типом; надо поддержать булево или, чтобы можно было задавать несколько разрешенных типов! В эликсире это сделано с помощью |, ну и мы сделаем так же. Может показаться, что это не так-то просто, но на самом деле, нет. Классы в Ruby позволяют переопределение метода #|: Что тут происходит? Достали два элемента из массива (этот и предыдущий,) склеили их,
сохраняя порядок и засунули обратно в массив аргументов: Не сложно. Еще, чтобы сделать код почище и избежать путаницы с порядком вызовов методов,
мы заставим пользователей эксплицитно оборачивать аргументы в скобки @spec[(foo), (bar | baz)]. Ну, вот тут я ожидал почти неразрешимых проблем. Разумеется, я мог бы использовать
hashrocket, как сделали бы ленивые неамбициозные рейлисты, но я не таков!
Мне хотелось добиться элегантности синтакса эликсира, с двоеточиями: Но как? — Да запросто. Как всем известно, ruby позволяет вызывать методы используя
не точку, а двойное двоеточие 42::to_s #⇒ "42", причем не только методы класса. Смотрите, как изящненько: двойное двоеточие просто делегирует вызов методу call
инстансу-ресиверу. Наша реализация просто вытащит последний аргумент из входного массива
(вся строка «выполняется» справа налево), и засунет в массив result.
Честно говоря, я думал, это будет сложнее. Тут вообще ничего делать не нужно: def, который идет вслед за аннотацией,
возвращает инстанс метода. Который и будет автоматически передан в качестве
аргумента тому, что вернет @spec[]. Поэтому мы просто вернем самое себя, и
полив метод в качествн входного аргумента — прицепим к нему аннотацию. Так просто. Фактически, все. Реализация готова к использованию. Ну, почти. Еще несколько
косметических добавлений, чтобы разрешить несколько разных вызовов @spec
(наподобие того, как desc в определении rake tasks собирает все определения
для дальнейшего использования), и документация. Хотелось бы предостеречь тех, кто побежал внедрять это прямо сейчас: не нужно
делать это ни дома, ни в школе, ни на работе. Не потому, что этот код сложен
(он прост), и не потому, что его сложно читать (его просто читать). Просто в реальной жизни это никому не нужно, ruby хорош именно тем, что он
супер, утка, динамичен, и типизировать его — только портить. Ну и засорять
глобальное пространство имен всяким шлаком, как это любят делать рельсы —
так себе практика. Если нужно внезапно найти у себя в простом классе миллиард
ненужных, неизвестно откуда взявшихся методов — ну, у нас уже есть ActiveSupport.
На наш век хватит. Код я привожу исключительно, как пример почти неограниченных возможностей ruby
выполнить любую прихоть сошедшего с ума разработчика. Вопросы, замечания, указания на ошибки? — С удовольствием отвечу и поспорю. Principal Engineer