Привет, Хабр! В рамках данной статьи мы создадим интерпретатор для простого языка программирования с использованием обобщённой свёртки. Далее следует небольшое введение. Для тех, кто уже знаком с типом Fix - фиксированной точкой для функторов, оставляю ссылку на основную часть статьи. Свёртка — операция, которая разрушает исходную структуру типа и возвращает одно значение. Например, преобразует список чисел в их сумму, при этом сама структура списка как бы разрушается. Рассмотрим правую свёртку для списка: Как можно заметить, функция f, которая передаётся первым аргументом, вызывается для каждого элемента списка, но при этом в ней не реализован рекурсивный обход списка. Получается, что в foldr реализуется обход списка и вызов нашей функции для каждого его элемента, нам же остаётся реализовать только непосредственно обработку. Возникает вопрос — можем ли мы написать функцию, аналогичную foldr,  которая будет работать для любого рекурсивного типа данных. Ответ — да, можем. Как, расскажу в следующих разделах. В данном разделе мы рассмотрим структуру данных список и выделим на его примере общий для всех рекурсивных типов паттерн. Давайте попробуем убрать рекурсию из данного типа, добавив параметр типа: Попробуем теперь создать список с двумя элементами: Прекрасно! У нас получилось — объявление типа списка теперь лишено рекурсии. Но есть одно но — xs имеет тип не List Int, как это было прежде, а List Int (List Int (List Int a)). То есть теперь мы не можем создать функцию, которая работает со списком, имеющим произвольный размер. На помощь нам приходит тип с фиксированной точкой. Раньше вместо параметра b в типе List мы подставляли List a, теперь если мы передадим в качестве параметра List a в Fix, конструктор Fix сделает то же самое за нас: Снова создадим список с двумя элементами: Можно радоваться — мы и рекурсию из типа убрали и тип теперь одинаков для любого количества элементов в списке. Аналогичные преобразования можно применить к любому рекурсивному типу, просто заменив его вхождения на параметр типа. Мы можем использовать второй параметр типа List, который ввели, чтобы исключить рекурсию из определения, для хранения значения аккумулятора. Рассмотрим пример склейки в строку всех значений списка с двумя элементами. Далее будет приведён код на псевдохаскеле, в каждом примере мы будем обрабатывать следующую часть списка: Первый шаг Преобразуем Nil в начальное значение аккумулятора, то есть в пустую строку: Второй шаг Преобразуем второй элемент списка и значение аккумулятора в новое значение аккумулятора: Третий шаг Преобразуем первый элемент списка и значение аккумулятора в новое значение аккумулятора: В результате получаем "12". В данном примере f — функция обработки списка, которую мы будем передавать в нашу свёртку. Когда идея того, как должна работать свёртка для типа Fix понятна, реализуем её. Тип должен быть функтором, чтобы мы могли проникать в его структуру и выполнять свёртку, начиная с конца. a в данном случае является типом аккумулятора. Значение аккумулятора всегда будет находится в тех местах, куда мы поместили параметр типа для избавления от рекурсии. При написании интерпретатора нам понадобится развёртка для типа Fix. Развёртка позволяет сгенерировать рекурсивный тип данных из одного начального: Стоит отметить, что в дальнейшем мы не будем использовать самописный тип Fix, так как он уже реализован в пакете data-fix. Итак, мы можем избавить определение любого рекурсивного типа данных от рекурсии и использовать функцию foldFix для преобразования всех значений в этом типе в одно. Рассмотрим плюсы данного подхода на примере интерпретации абстрактного синтаксического дерева (далее АСД):  Обработка синтаксического дерева без явного использования рекурсии позволяет избавиться от ошибок, связанных с тем, что мы забыли вызвать нашу функцию для одного из листов дерева. Например,  когда мы вызвали функцию в операторе If для условия и then блока, а для else забыли. Также это позволяет писать менее нагруженный код, так как явной рекурсии в коде больше не будет.  Возможность снабдить каждый лист дерева дополнительной информацией и гарантировать её наличие.  В нашем случае каждый лист дерева будет снабжён информацией о позиции внутри файла с программой, что  позволит нам печатать сообщения об ошибках вместе со строкой, на которой она произошла. Приступим к разработке интерпретатора. Поддерживаются только переменные с типом int и bool, условный оператор if, цикл while и рекурсивные функции. Пример программы приведён ниже: Напишем рекурсивный тип для представления АСД: Затем уберём рекурсию из определения типов: Нам может понадобится сравнивать выражения. Для типа Fix f определён инстанс класса Eq, если для f определён инстанс класса Eq1. Напишем определения Eq1 для типов ExprF, PrintArg и Ann. Про аппликативные парсеры есть куча хороших статей, поэтому я не буду останавливаться на полной реализации парсера, её можно посмотреть в репозитории. Здесь для примера того, как снабжать выражения позицией в файле, приведу код парсера для целочисленных констант: Для начала создадим монады, в которые будем интерпретировать программы. В одну монаду будут интерпретироваться выражения, снабжённые позицией, а в другую — не снабжённые. Это нужно, чтобы написать полиморфную функцию вывода ошибок. В одном случае она будет показывать строку с ошибкой, в другом — нет: Нам также понадобится класс типов для полиморфной функции возврата ошибки: Напишем инстансы для наших монад: Для PosInterpreterM ошибка будет выглядеть следующим образом: А для InterpreterM следующим: Далее напишем интерпретатор для выражений без позиции. Каждое выражение возвращает либо Int, либо Bool, либо ничего, поэтому тип возвращаемого значения m (Maybe Value): Самое интересное начинается сейчас — мы напишем интерпретатор для выражений с позицией, используя ту же функцию interpretExprF, что и для выражений без позиции. При этом перед вызовом interpretExprF для каждого листа синтаксического дерева, мы будем класть в контекст позицию этого листа в коде и затем убирать её из листа. Если бы мы использовали для представления синтаксического дерева обычный рекурсивный тип данных, нам пришлось бы явно класть в каждый лист позицию в коде: Для этого нам потребуется вспомогательная функция: Я позаимствовал её из проекта hnix. Основная идея заключается в том, что перед вычислением свёртки мы вызываем функцию g, которая позволяет сделать что-нибудь с текущим листом, например, модифицировать или извлечь информацию. В нашем случае мы будем доставать из листа позицию, класть её в контекст и после вызывать вычисление свёртки. Делать мы это будем в функции setContext: Соберём всё вместе и получим интерпретатор для выражений, снабжённых позицией: Теперь реализуем функцию interpretExprF. Сначала нужно будет реализовать функцию shouldSkip, которая будет вычислять выражение только в том случае, если переменные контекста ctxIsBreak и ctxIsReturn не установлены. Это нужно, чтобы инструкции после Break и Return не исполнялись: Написание интерпретатора начнём с числовых и булевых выражений. Мы просто преобразуем их в тип Value с помощью конструкторов IntValue и BoolValue соответственно: Далее переходим к переменным. Если удалось найти значение по имени переменной в хэш-таблице ctxVariables, то возвращаем его, иначе кидаем исключение: Далее будем интерпретировать унарные и бинарные операции. Для этого нам понадобятся три вспомогательные функции: Приступим к самим операциям. Я привёл код только трёх бинарных операций, потому что остальные реализуются похожим образом и не требуют детального описания: Далее по списку идёт операция присваивания. Для неё нужно написать ещё одну вспомогательную функцию: Код для самого присваивания приведён ниже: Теперь напишем реализацию условных выражений: Затем реализуем операции Break и Return: Для цикла while нам понадобятся следующие функции: Реализация цикла while выглядит следующим образом: Далее реализуем операцию Seq — последовательное исполнение команд. Следующий шаг — вызов функции. Напишем очередную вспомогательную функцию: Реализации вызова функции приведена ниже: Остаётся реализовать только ввод-вывод: Интерпретатор для выражений мы написали, однако программа состоит из функций, поэтому нужно написать интерпретатор для списка функций. Таких интерпретаторов будет два — один для функций, выражения в которых снабжены позицией, второй — для тех, в которых не снабжены: Все части интерпретатора написаны, осталось соединить их вместе: Итоговый код интерпретатора можно посмотреть в репозитории. Интерпретаторы императивных языков не самым лучшим образом реализуются посредством свёртки. Для корректной работы операций break и return пришлось применять хак с установкой флагов ctxIsBreak и ctxIsReturn и их проверкой перед исполнением каждой операции. Для раскрытия темы я выбрал интерпретатор императивного, как более наглядный и простой пример. В качестве более красивого применения описанного подхода, могу привести проверку типов, реализованную мной для двух языков: harakiri и tiger, трансляцию синтаксического дерева языка harakiri в промежуточное представление и проект hnix, в котором реализуется интерпретатор для функционального языка программирования nix.