Будет ли полезна возможность объявлять функции Lisp "чистыми"?

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

здесь мы go:

  1. хотелось бы что-то вроде (declare pure) потенциально помочь оптимизирующему компилятору? Или это спорный вопрос, потому что он уже знает?
  2. поможет ли объявление в доказательстве функции или программы или, по крайней мере, подмножества, которое было объявлено как чистое? Или это снова что-то ненужное, потому что это уже очевидно для программиста, компилятора и проверяющего?
  3. если ни для чего другого, было бы полезно программисту для компилятора обеспечить чистоту для функций с этим объявлением и добавить к удобочитаемости/ремонтопригодности программ Lisp?
  4. есть ли в этом смысл? Или я слишком устала, чтобы думать прямо сейчас?

Я бы признателен за любые идеи здесь. Информация о реализации компилятора или доказуемости приветствуется.

редактировать

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

3 ответов


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

во-первых, да, было бы, очевидно, хорошо знать, что функция чиста. Есть тонна вещей уровня компилятора, которые хотели бы знать это, а также вещи уровня пользователя. Учитывая, что языки lisp настолько гибкие, вы можете немного исказить вещи: вместо "чистого" объявления, которое просит компилятор стараться сильнее или что-то еще, вы просто делаете объявление ограничения код определение. Таким образом, вы можете гарантировать, что функция чисто.

вы даже можете сделать это с дополнительными вспомогательными средствами - я упомянул два из них в комментарии, который я сделал к ответу йоханбева: добавьте понятие неизменяемых Привязок и неизменяемых структур данных. Я знаю это в общие Lisp это очень проблематично, особенно неизменяемые привязки (так как CL загружает код путем "побочного эффекта" его на место). Но такие особенности помогут упростить вещи, и они не непостижимы (см., например,ракетка реализация, которая имеет неизменяемые пары и другие структуры данных и имеет неизменяемые привязки.

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

(define-pure (foo x)
  (cons (+ x 1) (bar)))

кажется, достаточно легко сказать, что эта функция действительно чист, он ничего не делает . Кроме того, кажется, что имея define-pure ограничить тело и разрешить только чистый код будет работать нормально в этом случае, и позволит это определение.

теперь начните с проблем:

  1. он звонил cons, поэтому предполагается, что он также известен как чистый. Кроме того, как я уже упоминал выше, он должен полагаться на cons то есть, предположим, что cons привязка является неизменным. Легко, так как это известное здание. Сделайте то же самое с bar, of курс.

  2. но cons тут имеют побочный эффект (даже если вы говорите о неизменяемых парах ракетки): это выделяет новая пара. Это кажется незначительной и игнорируемой точкой, но, например, если вы позволите таким вещам появляться в чистых функциях, вы не сможете их автоматически запомнить. Проблема в том, что кто-то может положиться на каждого foo вызов, возвращающий новую пару - ту, которая не является-eq к любому другому существующему пара. Кажется, что для этого вам нужно дополнительно ограничить чистые функции, чтобы иметь дело не только с неизменяемыми значениями, но и значениями, где конструктор не всегда создает новое значение (например, он может хэш-минусы вместо выделения).

  3. но этот код также называет bar -- так что нет, вам нужно сделать те же предположения на bar: он должен быть известен как чистая функция с неизменяемой привязкой. Обратите особое внимание, что bar не получает аргументов -- так что в этом case компилятор не мог требовать только этого bar является чистой функцией, она также может использовать эту информацию и предварительно вычислить ее значение. В конце концов, чистая функция без входов может быть сведена к простому значению. (Обратите внимание, кстати, что Haskell не имеет функций с нулевым аргументом.)

  4. и это приносит еще одну большую проблему. Что, если ... --5--> функция один вход? В этом случае у вас будет ошибка, и какое-то исключение будет выдано ... и это уже не чисто. Исключение составляют побочные эффекты. Теперь вам нужно знать arity bar в дополнение ко всему остальному, и вам нужно избегать других исключений. Теперь, как насчет этого ввода x -- что произойдет, если это не число? Это тоже вызовет исключение, поэтому вам тоже нужно избегать его. Это означает, что теперь вам нужна система типа.

  5. изменить (+ x 1) to (/ 1 x) и вы можете видеть, что вам нужна не только система типов, вам нужна та, которая достаточно сложный, чтобы различать 0s.

  6. наконец, есть еще один побочный эффект, который остается питой: что делать, если определение bar is (define-pure (bar) (bar))? Это, конечно, чисто по все вышеперечисленные ограничения... Но дивергирование-это форма побочного эффекта, поэтому даже это уже не кошерно. (Например, если вы заставите компилятор оптимизировать нулевые функции до значений, то в этом примере сам компилятор застрянет в бесконечном цикле.) (И да, Хаскелл не занимается этим, это не делает его менее проблемой.)


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

  1. Если пользователь может вручную аннотировать функцию как чистую, то компилятор может либо (a.) постарайтесь доказать, что функция является чистой, т. е. потратьте больше времени, прежде чем сдаться, или (b). предположим, что это так, и добавьте оптимизации, которые не были бы правильными для нечистых функций (например, для запоминания результатов). Итак, да, аннотирование функций как чистых может помочь компилятору, если аннотации считаются правильными.

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

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

  4. нет, кажется, это имеет смысл. Надеюсь, этот ответ тоже.


обычные вкусности применяются когда мы можем принять очищенность и referential прозрачность. Мы можем автоматически запоминать горячие точки. Мы можем автоматически распараллеливать вычисления. Мы можем справиться с состязание. Мы также можем использовать совместное использование структуры с данными, которые мы знать нельзя изменить, например (квази) примитивный `cons ()" не нужно копировать минусы-ячейки в списке, к которому он относится. На эти клетки никоим образом не влияет наличие другой минусы-клетки указывая на него. Этот пример довольно очевиден, но компиляторы часто хорошие исполнители в выяснении более сложной структуры обмена.

однако, фактически определяя, является ли лямбда (функция) чистой или имеет ссылочная прозрачность очень сложна в Common Lisp. Помнить это funcall (foo bar) начните с просмотра (symbol-function foo). Так что ... это дело

(defun foo (bar)
  (cons 'zot bar))

foo () является чистым.

следующая лямбда также чиста.

(defun quux ()
 (mapcar #'foo '(zong ding flop)))

однако, позже мы можем переопределить foo:

(let ((accu -1))
 (defun foo (bar)
   (incf accu)))

следующий вызов quux () больше не является чистым! Старый чистый foo () был redefined для того чтобы нечистая лямбда. Хлоп. Этот пример, возможно, несколько придумано, но это не так уж редко, чтобы лексически переопределить некоторые функции, например, с блоком let. В этом случае это не можно узнать, что произойдет во время компиляции.

Common Lisp имеет очень динамическую семантику, поэтому на самом деле способный определить поток управления и поток данных раньше времени (для экземпляр при компиляции) очень сложный и в большинстве полезных случаев совершенно неразрешимо. Это довольно типично для языков с динамической система типов. В Lisp есть много общих идиом, которые вы не можете использовать если необходимо использовать статический ввод. В основном эти фолы любой попытайтесь сделать много значимого статического анализа. Мы можем сделать это для примитивов как заключенные и друзья. Но для lambdas, связанных с другими вещами, чем примитивы мы находимся в гораздо более глубокой воде, особенно в тех случаях, когда нам нужно посмотрите на сложное взаимодействие между функциями. Помнить это лямбда чиста, только если все лямбды, которые она называет, также чисты.

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