Читая знаменитый SICP, я обнаружил, что авторы, скорее всего, неохотно представляют инструкцию присваивания Схеме в главе 3. Я читаю текст и понимаю, почему они так думают.
Поскольку Scheme является первым функциональным языком программирования, о котором я когда-либо знаю, я удивлен, что некоторые функциональные языки программирования (конечно, не Scheme) могут обходиться без заданий.
Давайте использовать пример, который предлагает книга, bank account
пример. Если нет оператора присваивания, как это можно сделать? Как изменить balance
переменную? Я спрашиваю об этом, потому что знаю, что существуют некоторые так называемые чисто функциональные языки, и согласно полной теории Тьюринга это тоже можно сделать.
Я изучил C, Java, Python и много использую назначения в каждой написанной мной программе. Так что это действительно открывающий глаза опыт. Я действительно надеюсь, что кто-то вкратце объяснит, как избегать назначений в этих функциональных языках программирования и какое глубокое влияние (если таковое имеется) оно оказывает на эти языки.
Пример, упомянутый выше, здесь:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
Это изменило свой balance
путь set!
. Для меня это очень похоже на метод класса для изменения члена класса balance
.
Как я уже сказал, я не знаком с функциональными языками программирования, поэтому, если я сказал что-то не так о них, не стесняйтесь указывать.
set!
или других функций, заканчивающихся на!
. Как только вы освоитесь с этим, переход на чистый FP должен быть более легким.Ответы:
Вы не можете изменять переменные без какого-либо оператора присваивания.
Не совсем. Если язык является полным по Тьюрингу, это означает, что он может вычислять все, что может вычислять любой другой полный язык по Тьюрингу. Это не значит, что он должен иметь все функции, которые есть у других языков.
Не противоречит тому, что полный язык программирования Тьюринга не имеет возможности изменить значение переменной, поскольку для каждой программы, которая имеет изменяемые переменные, вы можете написать эквивалентную программу, которая не имеет изменяемых переменных (где «эквивалентный» означает что он рассчитывает одно и то же). И на самом деле каждая программа может быть написана таким образом.
Что касается вашего примера: на чисто функциональном языке вы просто не сможете написать функцию, которая будет возвращать разный баланс счета при каждом вызове. Но вы все равно сможете переписать каждую программу, которая использует такую функцию, по-другому.
Поскольку вы запросили пример, давайте рассмотрим императивную программу, которая использует вашу функцию make -draw (в псевдокоде). Эта программа позволяет пользователю снимать со счета, вносить на него деньги или запрашивать сумму денег на счете:
Вот способ написать ту же программу без использования переменных-переменных (я не буду беспокоиться о ссылочно-прозрачном вводе-выводе, потому что вопрос был не об этом):
Эту же функцию можно было бы написать без использования рекурсии, используя сгиб над пользовательским вводом (который был бы более идиоматическим, чем явная рекурсия), но я не знаю, знакомы ли вы еще с сгибами, поэтому я написал это в таким образом, который не использует ничего, чего вы еще не знаете.
источник
newBalance = startingBalance + sum(deposits) - sum(withdrawals)
.Вы правы, что это очень похоже на метод объекта. Это потому, что это по сути то, что есть.
lambda
Функция представляет собой замыкание , которая тянет внешнюю переменнуюbalance
в ее рамки. Наличие нескольких замыканий, которые закрывают одну и ту же внешнюю переменную (и), и наличие нескольких методов для одного и того же объекта - две разные абстракции для выполнения одной и той же вещи, и любой из них может быть реализован в терминах другого, если вы понимаете обе парадигмы.Чисто функциональные языки обрабатывают состояние путем обмана. Например, в Haskell, если вы хотите прочитать входные данные из внешнего источника (который, конечно, является недетерминированным и не обязательно даст один и тот же результат дважды, если вы повторите его), он использует монадный трюк, чтобы сказать «мы получил эту другую притворную переменную, которая представляет состояние всего остального мира , и мы не можем исследовать это напрямую, но чтение входных данных - это чистая функция, которая принимает состояние внешнего мира и возвращает детерминированный входной сигнал, что это точное состояние всегда будет оказывать плюс новое состояние внешнего мира ". (Конечно, это упрощенное объяснение. Читая о том, как это на самом деле работает, вы серьезно сломаете свой мозг.)
Или в случае проблемы с вашим банковским счетом, вместо того, чтобы назначить новое значение переменной, он может вернуть новое значение как результат функции, а затем вызывающая сторона должна обработать его в функциональном стиле, обычно путем воссоздания любых данных. который ссылается на это значение с новой версией, содержащей обновленное значение. (Это не такая громоздкая операция, как может показаться, если ваши данные настроены на правильную структуру дерева).
источник
b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))
вы можете просто сделать ,b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));
гдеwithdraw
просто определяется какwithdraw(balance, amount) = balance - amount
.«Операторы множественного присваивания» являются одним из примеров языковой функции, которая, вообще говоря, имеет побочные эффекты и несовместима с некоторыми полезными свойствами функциональных языков (такими как ленивая оценка).
Это, однако, не означает, что назначение вообще несовместимо с чисто функциональным стилем программирования (см. Это обсуждение, например), и это не означает, что вы не можете создать синтаксис, который позволяет действиям, которые выглядят как назначения в целом, но реализованы без побочных эффектов. Однако создание такого синтаксиса и написание эффективных программ требует много времени и усилий.
В вашем конкретном примере вы правы - набор! Оператор является назначением. Это не свободный от побочных эффектов оператор, и это место, где Scheme ломается с чисто функциональным подходом к программированию.
В конце концов, любой чисто функциональный язык будет иметь разрыв с чисто функциональным подходом когда - то - подавляющее большинство полезных программ делает имеет побочные эффекты. Решение о том, где это сделать, обычно принимается за удобство, и разработчики языка будут пытаться предоставить программисту максимальную гибкость при выборе того, где следует остановиться, используя чисто функциональный подход, соответствующий их программе и проблемной области.
источник
На чисто функциональном языке можно было бы запрограммировать объект банковского счета как функцию преобразователя потока. Объект рассматривается как функция от бесконечного потока запросов от владельцев учетной записи (или кого-либо еще) к потенциально бесконечному потоку ответов. Функция начинается с начального баланса и обрабатывает каждый запрос во входном потоке для вычисления нового баланса, который затем возвращается к рекурсивному вызову для обработки оставшейся части потока. (Я помню, что SICP обсуждает парадигму потокового преобразователя в другой части книги.)
Более сложная версия этой парадигмы называется «функционально-реактивное программирование», обсуждаемое здесь на StackOverflow .
У наивного способа создания потоковых преобразователей есть некоторые проблемы. Возможно (на самом деле, довольно легко) написать глючные программы, которые хранят все старые запросы, тратя впустую пространство. Более серьезно, можно сделать ответ на текущий запрос зависимым от будущих запросов. Решения этих проблем в настоящее время разрабатываются. Нил Кришнасвами - сила, стоящая за ними.
Отказ от ответственности : я не принадлежу к церкви чистого функционального программирования. На самом деле я не принадлежу ни к какой церкви :-)
источник
Невозможно сделать программу на 100% функциональной, если она должна делать что-то полезное. (Если побочные эффекты не нужны, тогда весь процесс можно было бы свести к постоянному времени компиляции). Как и в примере с выводом, вы можете сделать большинство процедур функциональными, но в конечном итоге вам понадобятся процедуры, которые имеют побочные эффекты (ввод от пользователя, вывод на консоль). Тем не менее, вы можете сделать большую часть своего кода функциональной, и эту часть будет легко протестировать, даже автоматически. Затем вы создаете некоторый императивный код для ввода / вывода / базы данных / ..., который потребует отладки, но поддержание большей части кода в чистоте не составит большого труда. Я буду использовать ваш пример вывода:
Можно сделать то же самое практически на любом языке и получить одинаковые результаты (с меньшим количеством ошибок), хотя вам, возможно, придется устанавливать временные переменные в процедуре и даже изменять их, но это не имеет большого значения, если процедура фактически действует функционально (только параметры определяют результат). Я верю, что вы стали лучшим программистом на любом языке после того, как вы немного запрограммировали LISP :)
источник
Назначение - плохая операция, потому что оно делит пространство состояний на две части: до назначения и после назначения. Это вызывает трудности отслеживания того, как переменные изменяются во время выполнения программы. Следующие вещи в функциональных языках заменяют назначения:
источник