Использование unsafePerformIO во время выполнения интерпретатора языка программирования

Чтобы добавить функции ввода-вывода в интерпретатор языка программирования, написанный на Haskell, у меня есть два варианта:

  • Измените весь интерпретатор, чтобы он работал внутри монады IO.
  • В функциях среды выполнения, которые могут вызываться интерпретируемыми программами, используется unsafePerformIO.

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

Последнее меня нервирует — как следует из названия функции, это небезопасно, но я думаю, что в данной ситуации это может быть оправдано. Особенно:

  • Объем кода, затронутого этим изменением, будет очень небольшим.
  • Точки, в которых может выполняться ввод-вывод, уже явно упорядочены с помощью использования seq в контрольных точках во время оценки интерпретируемых выражений.
  • Возможно, что более важно, значения, возвращаемые действиями ввода-вывода, будут использоваться только в интерпретируемых разделах кода, где я могу гарантировать ссылочную прозрачность благодаря тому факту, что интерпретатор не может быть вызван несколько раз с одними и теми же аргументами, так как счетчик операций будет проходить через поток. всю систему как часть одного и того же изменения и всегда передается с уникальным значением каждой функции, которая будет использовать unsafePerformIO.

В этом случае есть ли веская причина не использовать unsafePerformIO?

Обновлять

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


person Jules    schedule 25.06.2016    source источник
comment
предполагая, что ваш интерпретатор будет взаимодействовать с пользователем - не будет ли он работать в IO в любом случае? Почему бы просто не обернуть IO значениями в представлении вашего интерпретируемого языка?   -  person Random Dev    schedule 25.06.2016
comment
Что сказал @Carsten. Если вы уже манипулируете значениями разных типов, то, возможно, один из этих типов может содержать значение ввода-вывода.   -  person Paul Johnson    schedule 25.06.2016
comment
Это именно то, как я собираюсь моделировать взаимодействие с пользователем, а также другие формы ввода-вывода. В настоящее время ввод-вывод задействован только во время начальной фазы загрузки модуля (которая происходит и завершается до выполнения). На данный момент кажется, что переход на использование ввода-вывода во время интерпретации потеряет по крайней мере некоторые преимущества работы в чистой системе. Избегание ввода-вывода, когда мне это не нужно, кажется полезной целью, и если я смогу выделить подсистему, которая действительно нуждается в этом, и доказать, что эта подсистема по-прежнему прозрачна с точки зрения ссылок , есть ли веская причина не делать этого?   -  person Jules    schedule 25.06.2016
comment
Предсказать, когда именно принудительно используются преобразователи в ленивом языке, может быть очень сложно — вот почему практически не существует ленивых нечистых функциональных языков. Добавление seq в нужных местах может быть менее тривиальным, чем кажется. Далее -- не могли бы вы написать фиктивный тип для интерпретатора? Это int :: Program -> Result или что-то подобное?   -  person chi    schedule 25.06.2016
comment
Общий тип интерпретатора, по сути, Scope s -> Expr -> ST s (Either ErrorMessage Value), где Scope s содержит описание как глобальных (изменяемых) переменных, так и функций/классов, определенных в программе, а Expr в основном представляет собой простое выражение начальной загрузки, которое вызывает функцию в программе.   -  person Jules    schedule 25.06.2016


Ответы (1)


Если я правильно понял, вы хотите добавить IO действий к интерпретируемому языку (нечистые примы), а сам интерпретатор чистый.

Первый вариант — это абстрактные примитивы от интерпретатора. Например, интерпретатор может работать в какой-то неопределенной монаде, в то время как priops вводятся:

data Primops m = Primops
  { putChar :: Char -> m ()
  , getChar :: m Char
  , ...
  }

interpret :: Monad m => Primops m -> Program -> m ()

Теперь интерпретатор не может выполнять никаких IO действий, кроме закрытого списка primops. (Вы можете добиться аналогичного результата, используя пользовательскую монаду вместо передачи primops в качестве аргумента.)

Но я бы посчитал это чрезмерным, пока вы точно не скажете, зачем вам нужен чистый интерпретатор. Наверное, нет? Если вы просто хотите упростить тестирование чистых частей интерпретатора, то, вероятно, лучше выделить эти части в отдельные чистые функции. Таким образом, точка входа верхнего уровня будет нечистой, но маленькой, но при этом всю логику интерпретатора можно будет проверить.

person Yuras    schedule 25.06.2016
comment
Это вариант, о котором я кратко подумал, но подумал, что это будет слишком сложно. Тем не менее, его, вероятно, стоит рассмотреть немного дольше, хотя бы потому, что у него есть несколько полезных приложений (например, запись трассировки операций ввода-вывода, чтобы упростить тестирование программ). Что касается причины, мне нужен чистый интерпретатор, я добавлю редактирование к вопросу. - person Jules; 25.06.2016
comment
Хорошо, вам нужны разные наборы примоп, так что инъекция их выглядит разумной. В любом случае, не позволяйте чрезмерным размышлениям помешать вам попробовать — вы, вероятно, все равно будете перепроектировать и реорганизовывать все несколько раз. - person Yuras; 25.06.2016