Возможны ли побочные эффекты в чисто функциональном программировании
Я уже давно пытаюсь обернуть голову функциональным программированием? Я искал лямбда-исчисление, LISP, OCML, F # и даже комбинаторную логику, но главная проблема, которую я испытываю, - это то, как вы делаете то, что требует побочных эффектов (взаимодействие с пользователем, общение с удаленным сервисом или даже управление моделирование с использованием случайной выборки), не нарушая фундаментальную предпосылку чистого функционального программирования, которая заключается в том, что для данного входа выходной сигнал является детерминированным? Надеюсь, у меня есть смысл, если не приветствую попыток должным образом воспитывать меня. Спасибо заранее.
Ответы
Ответ 1
Большинство функциональных программ реального мира не являются "чистыми" в большинстве чувств, поэтому половина ответа на ваш вопрос - "вы делаете это, отказываясь от чистоты". Тем не менее, есть альтернативы.
В "чистом" смысле чистого, вся программа представляет собой одну функцию из одного или нескольких аргументов, возвращая значение. Если вы прищурите глаза и немного помахаете руками, вы можете заявить, что все входные данные пользователя являются частью функции "аргументы" и что весь вывод является частью "возвращаемого значения", а затем немного подталкивает вещи, фактический ввод-вывод "по требованию".
Аналогичная перспектива заключается в том, чтобы объявить, что ввод функции является "всем состоянием внешнего мира" и что оценка функции возвращает новое измененное "состояние мира". В этом случае любая функция в программе, использующей мировое состояние, явно освобождается от детерминированности, поскольку никакие две оценки программы не будут иметь точно такой же внешний мир.
Если вы хотите написать интерактивную программу в чистом лямбда-исчислении (или что-то подобное, например, эзотерический язык Lazy K), это концептуально, как вы это сделаете.
Более практично, проблема сводится к тому, что ввод/вывод происходит в правильном порядке, когда ввод используется в качестве аргумента функции. Общая структура "чистого" решения этой проблемы - это функциональный состав. Например, скажем, у вас есть три функции, которые делают I/O, и вы хотите вызвать их в определенном порядке. Если вы делаете что-то вроде RunThreeFunctions(f1, f2, f3)
, то нечего определять порядок, в котором они будут оцениваться. С другой стороны, если вы позволяете каждой функции принимать другую функцию в качестве аргумента, вы можете связать их следующим образом: f1( f2( f3()))
, в в каком случае вы знаете, что f3
будет оцениваться первым, потому что оценка f2
зависит от его значения. [ Изменить: См. также комментарий ниже о ленивой и нетерпеливой оценке. Это важно, потому что ленивая оценка на самом деле довольно распространена в очень чистых контекстах; например, стандартная реализация рекурсии в чистом лямбда-исчислении не прерывается при ожидаемой оценке.]
Опять же, чтобы написать интерактивную программу в исчислении лямбда, так вы, вероятно, это сделаете. Если вы хотите что-то действительно пригодное для программирования, вы, вероятно, захотите объединить часть композиции функции с концептуальной структурой функций, принимающих и возвращающих значения, представляющие состояние мира, и создать некоторую абстракцию более высокого порядка для обработки конвейерной обработки "мировые государственные" значения между функциями ввода-вывода, в идеале также поддерживающие "мировое состояние", содержащиеся в целях обеспечения строгой линейности, - в этот момент вы все заново изобрели Haskell IO
Monad.
Надеюсь, это не просто смутило вас.
Ответ 2
Haskell - это чистый функциональный язык программирования. В Haskell все функции чисты (т.е. Они всегда дают один и тот же вывод для одних и тех же входов). Но как вы справляетесь с побочными эффектами в Haskell? Ну, эта проблема прекрасно решена с помощью монадов .
Ввод ввода-вывода в качестве примера. В Haskell каждая функция, которая выполняет I/O, возвращает IO-вычисление, то есть вычисление в монаде IO. Так, например, функция, которая считывает int с клавиатуры, вместо возвращения int, возвращает вычисление ввода-вывода, которое дает int при запуске:
askForInt :: String -> IO Int
Поскольку он возвращает вычисление ввода-вывода вместо Int
, вы не можете использовать этот результат непосредственно в сумме, например. Чтобы получить доступ к значению Int
, вам нужно "развернуть" вычисление. Единственный способ сделать это - использовать функцию bind (>>=
):
(>>=) :: IO a -> (a -> IO b) -> IO b
Поскольку это также возвращает вычисление IO, вы всегда получаете вычисление ввода-вывода. Вот как Haskell изолирует побочные эффекты. Мона монада действует как абстракция состояния реального мира (на самом деле под обложками обычно используется тип с именем RealWorld
для государственной части).
Ответ 3
Взаимодействие с пользователем и общение с удаленной службой требуют для вашего программного обеспечения своего рода нефункциональной части.
Многие "функциональные языки" (как и большинство Lisps) не являются чисто функциональными. Они по-прежнему позволяют вам делать что-то с побочными эффектами, хотя в большинстве случаев побочные эффекты "обескуражены".
Haskell является "чисто функциональным", но все же позволяет вам выполнять нефункциональные вещи через монаду IO. Основная идея заключается в том, что ваша чисто функциональная программа испускает ленивую структуру данных, которая оценивается нефункциональной программой (которую вы не пишете, это часть среды). Можно утверждать, что сама эта структура данных является императивной программой. Таким образом, вы выполняете обязательное метапрограммирование на функциональном языке.
Игнорируя, какой подход "лучше", целью в обоих случаях является создание разделения между функциональной и нефункциональной частями ваших программ и максимально возможное ограничение размера нефункциональных частей. Функциональные части имеют тенденцию быть более многоразовыми, проверяемыми и легче рассуждать.
Ответ 4
Функциональное программирование - это ограничение и выделение побочных эффектов, а не попытка полностью избавиться от них... потому что вы не можете.
... и да, я считаю, что FP полезен (конечно, с Erlang в любом случае): я считаю, что легче перейти от "идеи" к "программе" (или к решению;)... но, конечно, это могло бы просто будь я.
Ответ 5
Вам нужно знать хотя бы еще одну принципиальную концепцию: Monads. Вам понадобится это, чтобы делать I/O и другие "полезные" вещи!
Ответ 6
То, как Haskell делает это, используя монады, см. wikipedia и объяснение Haskell на их страница.
В принципе, идея состоит в том, что вы не избавляетесь от монады IO. Я понимаю, что вы можете связать функции, которые разворачивают монаду IO и выполняют эту функцию. Но вы вообще не можете удалить монаду IO.
Другим примером, использующим монады, которые напрямую не связаны с IO, является Maybe Monad. Эта монада является "непривлекательной" в отличие от монады IO. Но проще объяснить использование монадов с помощью монады Maybe. Предположим, что у вас есть следующая функция.
wrap :: Maybe x -> (x -> y) -> Maybe y
wrap Nothing f = Nothing
wrap (Just x) f = Just (f x)
теперь вы можете вызвать wrap (Just 4) (5+)
, который вернет Just 9
.
Идея IO-монады состоит в том, что вы можете использовать такие функции, как (+5) для внутреннего типа. Монада будет гарантировать, что функции будут вызываться в серийном режиме, потому что каждая функция привязана к оберточной IO-монаде.
Ответ 7
Единственный полностью чистый функциональный язык, о котором я знаю, - это система шаблонов в С++. Haskell занимает второе место, делая императивные части программы явными.
В Haskell программа имеет изменяемое состояние, но функции (почти всегда) этого не делают. Вы сохраняете как 99% процентов чистой программы, и только часть, которая взаимодействует с внешним миром, нечиста. Поэтому, когда вы тестируете функцию, вы знаете, что никаких побочных эффектов нет. Чистый сердечник с нечистой оболочкой.
Ответ 8
Учитывая, что большинство программ имеют некоторые эффекты для внешнего мира (запись в файлы, изменение данных в базе данных...), программы в целом редко являются побочными эффектами. Вне академических упражнений нет смысла даже пытаться.
Но программы собраны из строительных блоков (подпрограмма, функция, метод, назовите ее так, как вы хотите), а чистые функции делают для очень хорошо выполненных строительных блоков.
Большинство функциональных языков программирования не требуют, чтобы функции были чистыми, хотя хорошие функциональные программисты попытаются сделать так, чтобы многие из их функций были чистыми, насколько это возможно и практично, чтобы воспользоваться преимуществами ссылочной прозрачности.
Хаскелл идет дальше. Каждая часть программы Haskell чиста (по крайней мере, в отсутствие грехов, таких как "unsafePerformIO" ). Все функции, которые вы пишете в Haskell, являются чистыми.
Побочные эффекты вводятся через монады. Их можно использовать для введения своего рода "шоп-лист-покупатель" -сепарации. По сути ваша программа записывает список покупок (который является просто данными и может управляться по-чистому), а язык исполнения интерпретирует список покупок и делает эффективные покупки. Весь ваш код является чистым и дружественным к эквациональным рассуждениям и тому подобное, тогда как нечистый код предоставляется авторами-компиляторами.
Ответ 9
Даже если вы не используете его в своей работе, изучение одного или нескольких функциональных языков программирования - отличный способ научиться мыслить по-другому и дает вам инструментарий альтернативных подходов к проблемам (это также может расстроить вас, когда вы можете сделать что-то чистое и чистое как функциональный подход на других языках).
И это сделало меня лучше при написании стилей XSL.