Сохранение общего типа без η-разложения

Что я делаю: я пишу небольшую систему интерпретатора, которая может анализировать файл, превращать его в последовательность операций, а затем передавать тысячи наборов данных в эту последовательность для извлечения некоторого окончательного значения. от каждого. Скомпилированный интерпретатор состоит из списка чистых функций, которые принимают два аргумента: набор данных и контекст выполнения. Каждая функция возвращает измененный контекст выполнения:

type ('data, 'context) interpreter = ('data -> 'context -> 'context) list

Компилятор - это, по сути, токенизатор с последним этапом сопоставления токена с инструкциями, который использует описание карты, определенное следующим образом:

type ('data, 'context) map = (string * ('data -> 'context -> 'context)) list

Типичное использование интерпретатора выглядит так:

let pocket_calc = 
  let map = [ "add", (fun d c -> c # add d) ;
              "sub", (fun d c -> c # sub d) ;
              "mul", (fun d c -> c # mul d) ]
  in 
  Interpreter.parse map "path/to/file.txt"

let new_context = Interpreter.run pocket_calc data old_context

Проблема: я бы хотел, чтобы мой pocket_calc интерпретатор работал с любым классом, который поддерживает методы add, sub и mul и соответствующий тип data (могут быть целыми числами для одного класса контекста и числами с плавающей запятой для Другая).

Однако pocket_calc определяется как значение, а не функция, поэтому система типов не делает свой тип универсальным: при первом использовании типы 'data и 'context привязываются к типам любых данных и контекста, которые я сначала предоставляю, и интерпретатор навсегда становится несовместимым с любыми другими типами данных и контекстов.

Жизнеспособное решение - расширить определение интерпретатора, чтобы параметры его типа были универсальными:

let pocket_calc data context = 
  let map = [ "add", (fun d c -> c # add d) ;
              "sub", (fun d c -> c # sub d) ;
              "mul", (fun d c -> c # mul d) ]
  in 
  let interpreter = Interpreter.parse map "path/to/file.txt" in
  Interpreter.run interpreter data context

Однако такое решение неприемлемо по нескольким причинам:

  • Он повторно компилирует интерпретатор каждый раз при его вызове, что значительно снижает производительность. Даже этап сопоставления (превращение списка токенов в интерпретатор с использованием списка карт) вызывает заметное замедление.

  • Мой дизайн полагается на то, что все интерпретаторы загружаются во время инициализации, потому что компилятор выдает предупреждения всякий раз, когда токен в загруженном файле не соответствует строке в списке карт, и я хочу видеть все эти предупреждения при запуске программного обеспечения (а не когда отдельные в конечном итоге запускаются интерпретаторы).

  • Иногда я хочу повторно использовать данный список карт в нескольких интерпретаторах, будь то сам по себе или добавляя дополнительные инструкции (например, "div").

Вопросы: есть ли способ сделать тип параметрическим, кроме расширения eta? Может быть, какой-нибудь хитрый трюк с подписями модулей или наследованием? Если это невозможно, есть ли способ решить три проблемы, о которых я упомянул выше, чтобы сделать eta-расширение приемлемым решением? Спасибо!


person Victor Nicollet    schedule 25.10.2010    source источник
comment
Я добавил к своему ответу еще один раздел с более общим комментарием.   -  person gasche    schedule 25.10.2010


Ответы (2)


Жизнеспособное решение - расширить определение интерпретатора, чтобы параметры его типа были универсальными:

 let pocket_calc data context = 
   let map = [ "add", (fun d c -> c # add d) ;
               "sub", (fun d c -> c # sub d) ;
               "mul", (fun d c -> c # mul d) ]
   in 
   let interpreter = Interpreter.parse map "path/to/file.txt" in
   Interpreter.run interpreter data context

Однако такое решение неприемлемо по нескольким причинам:

  • Он повторно компилирует интерпретатор каждый раз при его вызове, что значительно снижает производительность. Даже этап сопоставления (превращение списка токенов в интерпретатор с использованием списка карт) вызывает заметное замедление.

Он перекомпилирует интерпретатор каждый раз, потому что вы делаете это неправильно. Правильная форма больше похожа на эту (и технически, если частичная интерпретация Interpreter.run в interpreter может сделать некоторые вычисления, вы должны переместить ее также из fun).

 let pocket_calc = 
   let map = [ "add", (fun d c -> c # add d) ;
               "sub", (fun d c -> c # sub d) ;
               "mul", (fun d c -> c # mul d) ]
   in 
   let interpreter = Interpreter.parse map "path/to/file.txt" in
   fun data context -> Interpreter.run interpreter data context
person Pascal Cuoq    schedule 25.10.2010
comment
Это не позволяет мне переместить let map в другой модуль, но я думаю, что смогу выжить и без этого. - person Victor Nicollet; 11.11.2010

Я думаю, ваша проблема заключается в отсутствии полиморфизма в ваших операциях, который вы хотели бы иметь закрытый параметрический тип (работает для всех данных, поддерживающих следующие арифметические примитивы) вместо параметра типа, представляющего фиксированный тип данных. Однако немного сложно обеспечить именно это, потому что ваш код недостаточно самодостаточен для его тестирования.

Предполагая данный тип для примитивов:

type 'a primitives = <
  add : 'a -> 'a;
  mul : 'a -> 'a; 
  sub : 'a -> 'a;
>

Вы можете использовать полиморфизм первого порядка, предоставляемый структурами и объектами:

type op = { op : 'a . 'a -> 'a primitives -> 'a }

let map = [ "add", { op = fun d c -> c # add d } ;
            "sub", { op = fun d c -> c # sub d } ;
            "mul", { op = fun d c -> c # mul d } ];;

Вы получаете следующий тип, не зависящий от данных:

 val map : (string * op) list

Изменить: относительно вашего комментария о различных типах операций, я не уверен, какой уровень гибкости вам нужен. Я не думаю, что вы могли бы смешивать операции над разными примитивами в одном и том же списке и при этом извлекать выгоду из особенностей каждого: в лучшем случае вы могли бы только преобразовать «операцию над add / sub / mul» в «операцию над add / sub / mul / div "(поскольку мы контравариантны в типе примитивов), но, конечно, немного.

На более прагматическом уровне верно то, что при таком дизайне вам нужен другой тип «операции» для каждого типа примитивов. Однако вы можете легко построить функтор, параметризованный типом примитивов и возвращающий тип операции.

Я не знаю, как можно выявить прямую связь подтипов между разными примитивными типами. Проблема в том, что для этого потребуется отношение подтипов на уровне функтора, чего, я не думаю, есть в Caml. Однако вы могли бы, используя более простую форму явного выделения подтипов (вместо преобразования a :> b, использовать функцию a -> b), построить второй функтор, контравариантный, который, учитывая отображение примитивного типа на другой, будет строить карту из одной операции типа к другому.

Вполне возможно, что с другим и умным представлением эволюционирующего типа возможно гораздо более простое решение. Первоклассные модули 3.12 также могут пригодиться, но они, как правило, полезны для первоклассных экзистенциальных типов, тогда как здесь мы чаще используем универсальные типы.

Накладные расходы на интерпретацию и уточнение операций

Помимо вашей локальной проблемы с набором текста, я не уверен, что вы идете правильным путем. Вы пытаетесь устранить накладные расходы на интерпретацию, создавая «заблаговременно» (перед использованием операций) замыкание, соответствующее представлению вашей операции на языке.

По моему опыту, этот подход обычно не избавляет от накладных расходов на интерпретацию, он скорее перемещает его на другой уровень. Если вы создадите замыкания наивно, поток управления синтаксического анализа будет воспроизведен на уровне замыкания: замыкание вызовет другие замыкания и т. Д., Поскольку ваш код синтаксического анализа "интерпретирует" ввод при создании замыкания. Вы устранили стоимость синтаксического анализа, но, возможно, неоптимальный поток управления остался прежним. Вдобавок, замыкания, как правило, сложно манипулировать напрямую: вы должны быть очень осторожны с операциями сравнения, например, сериализацией и т. Д.

Я думаю, что в долгосрочной перспективе вас может заинтересовать промежуточный «овеществленный» язык, представляющий ваши операции: простой алгебраический тип данных для арифметических операций, который вы построили бы из своего текстового представления. Вы все еще можете попытаться создать из него замыкания «раньше времени», хотя я не уверен, что производительность намного лучше, чем его прямая интерпретация, если представление в памяти приличное. Более того, будет намного проще подключить промежуточные анализаторы / преобразователи для оптимизации ваших операций, например, перейти от модели «ассоциативных бинарных операций» к модели «n-арных операций», которую можно было бы более эффективно оценивать.

person gasche    schedule 25.10.2010
comment
Спасибо. Это хорошо решило бы проблему для типа данных, хотя я не уверен, будет ли он поддерживать карту с add / sub / mul и другую с add / sub / mul / div без необходимости делать две карты полностью независимыми (и используя каждый раз другой примитивный тип). - person Victor Nicollet; 25.10.2010
comment
Re: Общий комментарий. У меня нет потока управления (замыкания никогда не вызывают других замыканий), инструкции просто выполняются по порядку. Моя проблема с представлением операций как типа заключается в том, что я определяю десятки новых (семантически различных) операций почти в каждом файле в моем проекте. Для использования типов потребуется несколько раз повторять стандартный код, просто говоря, что этот токен сопоставляется с этой функцией. - person Victor Nicollet; 11.11.2010