Действительно ли чисто функциональные языки гарантируют неизменность?

На чисто функциональном языке нельзя было еще определить оператор «присваивания», скажем «‹ - », так, чтобы команда, скажем,« i ‹- 3», вместо прямого присвоения неизменяемой переменной i, создавала копию всего текущего стека вызовов, за исключением замены i на 3 в новом стеке вызовов и выполнения нового стека вызовов с этого момента? Учитывая, что на самом деле никакие данные не изменились, разве это не будет считаться «чисто функциональным» по определению? Конечно, компилятор просто сделает оптимизацию, чтобы просто присвоить 3 переменной i, и в этом случае какая разница между императивным и чисто функциональным?


person Dax Fohl    schedule 09.04.2011    source источник


Ответы (3)


Haskell не дает вам способов самоанализа или «выполнения» стеков вызовов, так что я бы не стал особо беспокоиться об этой странной схеме. Однако в целом верно, что можно разрушить систему типов, используя небезопасные "функции", такие как unsafePerformIO :: IO a -> a. Идея состоит в том, чтобы сделать нарушение чистоты трудным, а не невозможным.

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

Есть предложение фактически гарантировать безопасность, объявив вне закона такие подрывные версии системы типов; Я не слишком знаком с этим, но вы можете прочитать об этом здесь.

person Tom Crockett    schedule 09.04.2011
comment
Хм, на самом деле я только что понял, что Haskell не только не дает вам способа выполнять стеки вызовов, но я думаю, что любой чистый язык не может - стек вызовов постоянно меняется, поэтому если бы была функция GetCallStack, то это зависело бы от состояния, что, как я думаю, было бы нарушением чистоты. Мой небольшой обходной путь неявно вызывает этот GetCallStack, поэтому похоже, что мой аргумент неверен. Если, конечно, не существовала монада состояния, отслеживающая стек вызовов для каждого вызова функции, но в этом случае я в любом случае просто пишу интерпретатор. - person Dax Fohl; 10.04.2011
comment
@Dax: Верно, для того, чтобы гипотетический getCallStack :: Stack был хоть сколько-нибудь полезен, он должен был бы давать разные Stack в зависимости от того, где вы были в своей программе, что, очевидно, нарушает ссылочную прозрачность. - person Tom Crockett; 10.04.2011

У чисто функциональных языков, таких как Haskell, есть способы моделирования императивных языков, и они не стесняются это признавать. :)

См., В частности, http://www.haskell.org/tutorial/io.html 7.5:

Итак, в конце концов, неужели Haskell просто заново изобрел императивное колесо?

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

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

Конечно, вы можете проигнорировать это и написать всю свою программу в императивном стиле, но тогда вы не воспользуетесь возможностями языка, так зачем его использовать?

Обновить

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

Но, конечно, вместо этого вы просто пишете функцию, которая действует как тело цикла, а затем заставляете ее вызывать себя. Каждый вызов функции соответствует «шагу итерации». И в рамках каждого вызова параметр имеет другое значение, действуя как увеличивающаяся переменная. Наконец, среда выполнения может заметить, что рекурсивный вызов появляется в конце вызова, и поэтому она может повторно использовать верхнюю часть стека вызовов функций вместо того, чтобы увеличивать ее (хвостовой вызов). Даже в этом простом шаблоне есть почти весь вкус вашей идеи - включая компилятор / среду выполнения, незаметно вмешивающуюся и фактически вызывающую мутацию (перезапись вершины стека). Это не только логически эквивалентно циклу с изменяющимся счетчиком, но и фактически заставляет процессор и память делать то же самое физически.

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

person Daniel Earwicker    schedule 09.04.2011
comment
Да, но я думал, что моя схема будет работать даже вне монады ввода-вывода, поскольку она действительно не имеет никаких побочных эффектов. Однако я заметил в своей аргументации изъян, который выделил в комментарии к пелотому. - person Dax Fohl; 10.04.2011
comment
@Update Хотя я не видел никаких абсолютных доказательств, я нашел несколько статей, которые я нашел при поиске отчета о чистоте callcc, о том, что CallCC отрицает всю функциональную чистоту. Хотя я недостаточно умен, чтобы доказать, что это так, я должен сказать, что это имеет смысл, учитывая, что CC, хотя и не что иное, как указатель на функцию, тем не менее, является изменяющейся неявной контекстной переменной, передаваемой в функцию. Как я сказал в другом комментарии, это сработало бы, если бы у вас была специальная монада состояния продолжения, но, если я не ошибаюсь, это более или менее сводится к написанию вашего собственного интерпретатора. - person Dax Fohl; 10.04.2011
comment
@Dax: Мне это кажется верным, но если вы посмотрите на тип Haskell _ 1_, это MonadCont m => ((a -> m b) -> m a) -> m a. Монада продолжения улавливает семантику продолжений и, таким образом, позволяет вам работать таким образом (пока вы остаетесь внутри нее). - person Antal Spector-Zabusky; 10.04.2011

Неизменяемость - это свойство языка, а не реализации.

Операция a <- expr, которая копирует данные, по-прежнему является обязательной операцией, если значения, относящиеся к местоположению a, кажутся измененными с точки зрения программистов.

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

person antonakos    schedule 09.04.2011
comment
Более вероятная оптимизация: если предыдущий список не нужен, вероятно, он будет объединен с потоком. - person alternative; 10.04.2012