Логос-ориентированный Lisp компилируется в Javascript
Сегодня компиляторы компьютерных программ анализируют только грамматику языка и игнорируют семантику каждой конкретной программы. В итоге мы получаем исходные тексты с высоким уровнем избыточности, что c ростом размеров программы приводит к закономерным сложностям с поддержкой. Любые серьезные изменения в программе становятся практически невозможными без появления большого числа регрессий и новых ошибок.
Представьте, что вы можете формализовать некоторые аспекты именно создания компьютерных программ. Не сам код, а именно процесс создания кода. Тогда компилятор будет не просто разбирать и компилировать код, но частично понимать смысл вашего кода и генерировать новый код в зависимости от окружающего контекста.
Например, у вас есть функция save-thing
, требующая два
обязательных параметра: thing
и db
.
(defn save-thing (thing db))
Где-то в программе вы вызываете эту функцию без параметров
(save-thing)
, но компилятор из описания функции знает,
что этой функции необходимы thing
и db
,
находит эти параметры в текущем лексическом контексте и генерирует
полный вызов, например:
(save-thing user-thing environment.db)
В другом месте программы (и другом контексте) компилятор для той же
исходной строки (save-thing)
сгенерирует другой вызов,
например:
(save-thing backup-thing (get-secondary-db))
Если вы укажете все необходимые параметры вызываемой функции,
компилятор не будет додумывать за вас, а просто проверит
соответствие переданных значений ожидаемым параметрам. То, какой из
доступных в контексте вызова символов использовать в качестве
пропущеного параметра функции, может определяться несколькими
способами. Самый простой способ, по имени: можно искать в контекте
вызова символ с именем thing
. Очень часто это будет
параметр функции, определенной на уровень выше, например:
(defn process-thing (thing)
(log thing)
(save-thing))
Аналогично, можно искать соотвествия в мета-информации, связаной с параметрами функции и символами контекста вызова. Такие соответствия могут основываться на статической типизации (используется в TypeScript) или ассоциативных связях (используется в MetaJS).
Таким образом, одна и таже написанная программистом строка
(save-thing)
может быть преобразована в разные вызовы
этой функции еще на уровне исходного кода (формально после
развертывания синтаксических макросов, но до трансляции из
мета-языка в целевой язык).
Принципиальное отличие семантических трансформаций кода от синтаксических макросов, используемых различными диалектами Lisp, состоит в том, что код, который генерирует макрос, зависит только от параметров, с которыми он вызван, а сценарий семантиченской трансформации формируется компилятором на лету в зависимости от лексического контекста вызова и доступных логосов. Иногда макросы могут изменять поведение в зависимости от глобальных переменных типа enable-assert, но это исключение скорее подтверждает правило, потому что сценарий трансформации всегда жестко задан в самом макросе.
При логос-ориентированном программировании вы общаетесь с компилятором преимущественно глаголами (названиями функций), а где взять параметры для этих функций, компилятор определяет самостоятельно на основании сигнатуры функции, контекста вызова, неявных семантических правил и явных, задаваемых в логосах, импортируемых в используемое пространство имен.
Логос программы это главная особенность MetaJS, которая делает всю эту магию возможной. Можно даже сказать больше: сам MetaJS это лишь демонстрация возможности логос-ориентированного программирования и его первая реализация. Сама концепция логос-ориентированного программирования может быть адаптирована и для других языков программирования.
MetaJS использует синтаксис Lisp из-за его уникального свойства — саморепрезентативности больше известного как формула code is data. Это свойство, как нельзя лучше подходит для компилятора, трансформирующего и расширяющего исходный код.
Логос-ориентированное программирование в отличие от объектно-ориентированного или функционального во главу угла ставит не основные строительные блоки программы (объекты или функции), а семантические модели разрабатываемой программы в форме, которую может понять и использовать компилятор. Такие семантические модели и называются логосами.
Конечная программа использует много логосов. Например, каждая библиотека может иметь свой логос, описывающий порядок ее использования и семантические связи между функциями библиотеки и понятиями, которыми оперируют эти функции. Также каждая предметная область может иметь свой логос, описывающий ее понятия и связи между ними.
Логосы могут быть вообще не связаны с программным кодом. Один и тот
же логос предметной области может использоваться разными
библиотеками этой предметной области, а связь между понятиями логоса
предметной области и конкретными функциями библиотеки задается в
логосе библиотеки. В самом приложении используется логос предметной
области и поэтому, если программист решает заменить одну библиотеку
на другую компилятор самостоятельно генерирует вызовы новых функций
из новой библиотеки передав новые параметры. В случаях, когда
произвести такую замену автоматически невозможно, компилятор
запрашивает помощь у программиста, также как git
делает
слияние изменений автоматически, но при обнаружении конфликтов
передает контроль программисту.
Например, ваша программа может использовать логос
информационного протокола Coect (проекта,
для которого MetaJS тоже самое, что ELisp для Emacs) и оперировать
высокоуровневыми операциями (send-message)
(like-message)
(load-timeline limit:20)
, а
конкретные вызовы функций, отвечающих за реализацию этих
высокоуровневых операций будет генерировать компилятор. Это в
теории.
На практике, компилятор не всегда по контексту может определить, что конкретно должно быть сделано. Например, в текущем контексте доступно несколько объектов, подходящих на роль message и непонятно, какую переменную передавать в функциию like-message, а это, согласитесь, важно.
Здесь, как обычно, есть несколько путей. Первый путь — явно указать, какую переменную использовать (like-message my-friend-cat). Второй путь — изменить структуру программы, разбив ее на меньшие функции, каждая из которых в духе UNIX way делает только одно действие, оперируя минимально возможным контекстом.
Третий путь, если это ситуация типичная и повторяется в разных местах программы, можно добавить в логос программы специальное правило для этой ситуации, и компилятор в следующий раз будет знать, как ее решить без помощи программиста.
Важно отметить, что логос-ориентированное программирование не противопоставляется объекто-ориентированному или функциональному. Вы продолжаете писать в том же стиле, как и писали раньше, но также объясняете компилятору ваш стиль и предметную область. Постепенно компилятор учится дописывать код в вашем стиле по вашим правилам за вас.
Каждый программист может записывать свои приемы программирования в виде правил логоса и настраивать компилятор под себя. Но тот же компилятор может прочитать логосы других программистов и логосы предметных областей, используемых в проекте. То, что в конце концов получится, наверное, уже нельзя будет назвать просто компилятором. Это будет уже новый член команды, скучный, временами туповатый, но очень производительный...
Дмитрий Догадайло,
github:dogada, twitter:@d0gada.
Перечитав текст статьи, я нашел, что местами он читается, как научная фанстастика, но часть описанной здесь функциональности уже реализована и доступна на GitHub. Вы также можете попробовать MetaJS, не покидая броузера, на странице интерактивной документации. Там же демонстрируется, как MetaJS генерирует недостающий код.
Каждый радист знает: закончил передачу — переходи на прием. Напишите нам, что вы думаете о проекте Coect, что нужно именно вам в первую очередь.
Вы можете связаться с нами через Твиттер @coect_net или страницу Coect на Фейсбук.
Следите за обновлениями кода MetaJS на GitHub.
Есть идея? Добавьте свои пожелания через UserVoice.
Присоединяйтесь к нашему списку рассылки.
Черкните нам пару строк по адресу info at dogada.org.
Главные новости о Coect и MetaJS также публикуются в твиттере Дмитрия Догадайло @d0gada.
До coect'a!