Во многих языках есть специальный механихм для кодогенерации - макросы. Иногда их реализуют на отдельном достаточно примитивном языке, основанном на простой подстановке текста (препроцессоры PL/I и C, m4), но даже в таком варианте удается делать интересные и полезные вещи. Другой популярный вариант - макросы реализуются на том же языке, что и программа, в которой они используются. Такой подход ведет свое начало из Lisp (удобный тем, что формат программ и данных там одинаков), активно применяется в Julia, OCaml(camlp4/5), Scala, Haskell, Rust, а наибольшего развития получил в Nemerle, где макрос может может запускаться как до, так и после проверки и вывода типов, и в последнем варианте иметь доступ к типам.
При этом в макросах все возможности языка не нужны, скажем высокая эффективность и безопасность Rust пользы здесь не принесут и могут только затруднить разработку.
К тому же все это функциональные или императивные языки, и мне стало интересно, что бы мог дать логический язык, в плане написания макросов. Появилась идея реализовать Prolog на Nemerle, но внезапно выяснилось, что последний, несмотря на заботу одной большой и известной компании, умер, и компилятор сигфотлится на доступных версиях mono. Тогда я решил (надеюсь, временно) снизить планку и попробовать всего лишь функциональный, но сравнительно легко встраиваемый Gluon. Макросы в Rust приходится оформлять отдельным пакетом, что не очень удобно, если макрос хочется разработать для однократного применения. Так почему бы не реализовать макрос, который просто получает как параметр код на Gluon и его выполняет?
Сейчас Gluon должен вернуть строку, которая будет парсится как код на Rust. Хотелось бы конечно уметь возвращать массив токенов, но я не сумел протянуть в Gluon необходимые типы.
Я думаю такой подход может быть востребован для "одноразовых" макросов. Пусть мы хотим инициализировать статический массив нулями, кроме нескольких заданных единиц. Можно просто написать
static MASK: [i32; 64] = glumacro::a_proc_macro!(r#"
let array = import! std.array
let s = [13, 17, 42]
let inl x = array.foldable.foldr (\y a -> a || x == y) False s
rec let f n = if n == 64
then []
else array.append [if inl n then 1 else 0] (f (n+1))
in show (f 0)
"#);
В чуть более сложном примере создается массив простых чисел - использовать макрос тут удобно, но задача не стоит того, чтобы заводить под нее отдельный пакет, а написать генерирующий код на месте можно.
Комментарии (13)
NN1
16.10.2021 22:56Могу помочь с запуском Nemerle в Mono.
О какой версии Mono идёт речь ?
potan Автор
17.10.2021 01:42+1Mono JIT compiler version 6.12.0.90 (tarball Mon Sep 13 15:34:59 UTC 2021)
Компилятор падает где-то в глубинах glibc с диагностикой "Assertion at sre-encode.c:290, condition `count > 0' not met".
NN1
17.10.2021 17:18+1На какой системе запускаете, чтобы можно было попробовать воспроизвести.
Mono, увы, периодически чинят баги, а потом снова ломают. В какой-то момент я от них устал и проверял периодически, а сейчас уже не смотрю, но посмотрю для вас.
Если есть возможность завести баг в соответствующем месте будет совсем замечательно: https://github.com/rsdn/nemerle
NN1
17.10.2021 17:21+2Ну вот, оказывается известный баг Mono: https://github.com/mono/mono/issues/18970
Есть полный стек компилятора, вдруг сходу придумается обход?
potan Автор
17.10.2021 17:59Да, стектрей похож. Такое чувство, что пора переписывать .net на Rust...
Кстати, про Boo мне говорили, что там боже были хорошие макросы, но с документацией там совсем плохо.
NN1
17.10.2021 18:07Проблема не с .NET , а с конкретной реализацией т.е. Mono :)
До недавнего времени .NET Core не поддерживал нормальную работу с System.Reflection.Emit на которую завязан компилятор Nemerle.
Вполне возможно, что портировать на .NET 5 или 6 это гораздо более посильная задача чем портирование на .NET Core.
warlock13
17.10.2021 09:27+3в макросах все возможности языка не нужны, скажем высокая эффективность и безопасность Rust пользы здесь не принесут и могут только затруднить разработку
Спорно. Ту же "безопасность" можно назвать и слегка иначе: простой способ получить действительно работающий, а не кажется-что-работающий-но-на-самом-деле-нет макрос.
potan Автор
17.10.2021 17:55Результат вызова макроса валидируется, то есть проходит через тайпчекер и тесты. Rust защищает от некорректного использования памяти, гонок, переполнений буфера, то есть достаточно низкоуровневых ошибок. Более высокоуровневые ограничения легче выражались бы в других системах типов, в том числе в Gluon.
warlock13
18.10.2021 16:39+1Rust защищает от некорректного использования памяти, гонок, переполнений буфера, то есть достаточно низкоуровневых ошибок.
На техническом уровне - да. Но есть ещё связанный с ним крайне важный культурный уровень. При программировании на каком ещё ЯП вы волей-неволей узнаете, что путь к файлу в Windows не является в общем случае юникодной строкой? Вы можете выразить соответствующие вещи в мощных системах типов, но _будете ли_?
potan Автор
18.10.2021 17:33Я думаю, более важно, чтобы это было отражено в типах целевого языка, а не языка разработки макроса.
SShtole
Не хватает примеров в тексте статьи ) А тема в целом очень интересная.
potan Автор
Да, Вы правы. Добавил простой пример использования. Спасибо за совет.