Возможности DSL на основе AST в представлении рекурсивных структур
Уже около года пишу Go, и столько же времени занимаюсь реализацией синтаксических деревьев. Сейчас все идет хорошо, но когда я начинал, это была настоящая борьба из-за того, что я не нашел никакого контента для начинающих, посвященного этой теме. Итак, это я делаю одолжение себе и, надеюсь, некоторым из вас.
Одной из полезных особенностей предметно-ориентированных языков на основе синтаксического дерева является их рекурсивный характер, что делает их превосходными для представления математических областей, таких как логика и алгебра. Таким образом, в качестве рабочего примера мы напишем программу, которая может символически представлять основные алгебраические выражения, а также манипулировать ими и вычислять их. Если вы не помешаны на математике, как я; Не волнуйтесь! В основном я сосредоточусь на деталях реализации нашей программы, а не на математике.
Предпосылки
Доменный язык (DSL)
Вам не нужно быть экспертом по предметно-ориентированным языкам (DSL), чтобы следовать этому курсу. На самом деле вам не нужно ничего знать о нем, кроме того, что это язык с меньшей областью применения, чем язык общего назначения (GPR), такой как Go. Это означает, что DSL оптимизирован для очень эффективного решения проблем в одной конкретной области (как с точки зрения вычислений, так и с точки зрения реализации) и может даже не поддерживать выполнение каких-либо действий за пределами этой области.
Поскольку DSL имеют очень ограниченную функциональность, они часто реализуются на других языках. Следовательно, пакет для построения SQL-запросов можно рассматривать как DSL, где предметной областью являются SQL-запросы. В случае нашего рабочего примера предметной областью будут алгебраические выражения, реализованные в виде пакета Go.
Абстрактное синтаксическое дерево (AST)
Абстрактное синтаксическое дерево (AST) — это структура данных, используемая для представления абстрактной синтаксической структуры некоторых данных. Для нас это означает, что, как следует из названия, AST представляет структуру и значение некоторых данных в виде дерева. Тип данных, который легко моделируется с помощью AST, обычно является рекурсивным, например. алгебраические выражения или исходный код. AST обычно используются в компиляторах, поскольку они позволяют представить значение.
Для наших намерений и целей достаточно понять, что алгебраическое выражение, т.е. 4 + 6x
, представленный на рисунке 1, хорошо вписывается в древовидную структуру. Точнее, каждый оператор (+
, -
, *
, /
, Const
, x
) представлен узлом, а его аргументы, если они есть, представлены дочерними узлами.
Сочетание DSL и AST
Имея представление о том, что такое DSL и AST, мы можем собрать их воедино, чтобы сформировать идею, лежащую в основе нашего рабочего примера предметно-ориентированных языков на основе синтаксического дерева для простых алгебраических выражений.
Мы хотим иметь возможность представлять любое алгебраическое выражение одной переменной, содержащее любой или все основные арифметические операторы (+
, -
, *
, /
), и нам нужен какой-то способ вычисления данного выражения по некоторому заданному значению. Все это проиллюстрировано на рис. 2, где выражение 4 + 6x
представлено в виде AST, а процесс его вычисления в x = 10
, приводящий к 4 + 6*10 = 64
, визуализируется.
Выполнение
Моделирование дерева с использованием структур
Наиболее важным инструментом при создании DSL на основе синтаксиса является система типов Go. Однако прежде чем приступить к построению древовидной структуры нашего AST, мы рассмотрим каждый узел на рисунке 1 и поймем, что существует два основных класса различных узлов; с детьми и без. Первый состоит из основных арифметических операций (+
, -
, *
, /
). Все они принимают два аргумента, которые в нашей древовидной структуре соответствуют своим дочерним элементам. Узлы без дочерних элементов — это листья нашей древовидной структуры, и можно увидеть, что они состоят из констант и переменных.
Затем мы понимаем, что каждый из различных типов узлов, по отдельности или связанных для формирования дерева, может рассматриваться как алгебраическое выражение. Поэтому мы определяем интерфейс как:
Чтобы алгебраические выражения печатались красиво, мы разрешили этому интерфейсу реализовать String
-метод.
Теперь, с идентифицированными различными типами узлов и с интерфейсом, который улавливает их сходство, мы можем с помощью встраивания типов определить все различные типы узлов как структуры:
Здесь внедрение типа относится к безымянному полю структуры типа Expr
в верхней части каждой структуры. По сути, это аналог наследования в Go и говорится, что каждый тип узла также является Expr
. Это позволяет определить дочерние элементы или аргументы для узлов любого типа, включающего интерфейс Expr
.
Вот подробности реализации метода String
:
Со всем этим реализованным мы можем построить выражение, показанное на рисунке 1, и распечатать его с помощью программы ниже.
> go run main.go ( 4 ) + ( ( 6 ) * ( X ))
Eval-функция
Возможность представить алгебраическое выражение в Go с помощью структур и встраивания типов — это круто, но мы также хотели бы оценить его для заданного значения x
. Для этого мы расширим интерфейс Expr
, включив в него функцию Eval
:
Поведение этой функции показано на рисунке 2; он принимает синтаксическое дерево и значение для x
и возвращает выражение, оцененное по этому значению. Чтобы реализовать это, мы используем рекурсию, давая каждому типу узла свою собственную реализацию Eval
с двумя типами листовых узлов (Const
и x
), которые являются базовыми для рекурсии, поскольку у них нет дочерних узлов. В случае нелистовых узлов функция Eval
заменяет синтаксическую операцию той, которая фактически используется в Go, например. Add
становится +
, а затем рекурсивно делает то же самое с дочерними узлами. На практике это выглядит следующим образом:
Помимо представления алгебраического выражения и его вывода на терминал, теперь мы также можем оценить его по определенному значению. Пример того, как это сделать, показан в программе ниже:
> go run main.go ( 4 ) + ( ( 6 ) * ( X )) = 64
Преобразование синтаксического дерева
Хорошо, мы можем представить алгебраическое выражение в виде древовидной структуры данных в нашей программе и оценить его для заданного значения, и что? Почему бы нам просто не написать выражение как лямбда-функцию напрямую, избавившись от проблем с деревьями и рекурсией? Я надеялся, что вы спросите, потому что мы еще не видели самого удивительного в синтаксических представлениях.
Допустим, по какой-то причине вам нужно вычислить производную нашего примерного выражения 4 + 6x
. Если бы мы представили это выражение в виде лямбда-функции, мы могли бы только аппроксимировать производную для заданного значения x
, но с нашей DSL на основе AST мы действительно можем выполнить дифференцирование в соответствии со всеми правилами, которые мы изучили на уроке математики, и таким образом, получить точную производную, которую мы затем можем вычислить в любой точке.
Чтобы сделать это возможным, нам снова нужно расширить наш интерфейс Expr
функцией дифференцирования D
:
Затем, как и в случае с Eval
-функцией, мы можем рекурсивно реализовать правила дифференцирования для каждого из различных операторов:
Затем мы можем дифференцировать наше выражение, запустив следующую простую программу:
> go run main.go D(( 4 ) + ( ( 6 ) * ( X ) )) = ( 0 ) + ( ( ( 0 ) * ( X ) ) + ( ( 6 ) * ( 1 ) ) )
В выводе много избыточных терминов, что связано с тем, что мы никогда не упрощаем наше выражение, а получаем то, что ожидаем; D(4 + 6x) = 0 + 0*x + 6*1 = 6
.
Заключение
Теперь мы увидели, насколько полезными могут быть DSL на основе AST для представления рекурсивных структур, особенно алгебраических выражений, и как их реализовать в Go. Мы также видели, насколько полезными могут быть синтаксические представления при выполнении символических преобразований, таких как дифференцирование.
Если вы нашли это интересным, вам повезло, потому что с AST и DSL можно делать гораздо больше. Я впервые столкнулся с этой темой в курсе предметно-ориентированных языков математики в Технологическом университете Чалмерса, который является курсом с открытым исходным кодом, и его содержание можно найти в этом репозитории G ithub. Тем не менее, содержание курса довольно насыщено математикой, но ссылки в файле README репозитория содержат некоторую литературу, которая охватывает эту тему без особой математики.