Я уже давно пытаюсь осмыслить функциональное программирование? Я искал лямбда-исчисление, LISP, OCML, F # и даже комбинаторную логику, но основная проблема, с которой я столкнулся, заключается в том, как вы делаете то, что требует побочных эффектов, таких как (взаимодействие с пользователем, общение с удаленной службой или даже обработка моделирования с использованием случайная выборка), не нарушая фундаментальную предпосылку чистого функционального программирования, заключающуюся в том, что для заданного входа выход является детерминированным? Я надеюсь, что у меня есть смысл, иначе я приветствую любые попытки меня должным образом обучить. Заранее спасибо.
Возможны ли побочные эффекты в чисто функциональном программировании
Ответы (9)
Большая часть реального функционального программирования не является «чистым» во многих смыслах, поэтому половина ответа на ваш вопрос - «вы делаете это, отказываясь от чистоты». Тем не менее, есть альтернативы.
В «чистом» смысле вся программа представляет собой единственную функцию одного или нескольких аргументов, возвращающую значение. Если вы прищурите глаза и немного помашите руками, вы можете объявить, что весь пользовательский ввод является частью «аргументов» функции, и что весь вывод является частью «возвращаемого значения», а затем немного подделать вещи, чтобы он выполнял только фактический ввод / вывод «по запросу».
Аналогичная перспектива состоит в том, чтобы объявить, что входные данные функции - это «все состояние внешнего мира» и что оценка функции возвращает новое, измененное «состояние мира». В этом случае любая функция в программе, которая использует состояние мира, очевидно, свободна от «детерминированности», поскольку никакие две оценки программы не будут иметь абсолютно одинаковый внешний мир.
Если бы вы хотели написать интерактивную программу на чистом лямбда-исчислении (или что-то подобное, например, на эзотерическом языке Lazy K), концептуально вы бы это сделали.
В более практическом плане проблема сводится к тому, чтобы убедиться, что ввод-вывод происходит в правильном порядке, когда ввод используется в качестве аргумента функции. Общая структура «чистого» решения этой проблемы - это композиция функций. Например, предположим, что у вас есть три функции, которые выполняют ввод-вывод, и вы хотите вызывать их в определенном порядке. Если вы сделаете что-то вроде RunThreeFunctions(f1, f2, f3)
, нет ничего, чтобы определить порядок, в котором они будут оцениваться. С другой стороны, если вы позволите каждой функции принимать другую функцию в качестве аргумента, вы можете связать их следующим образом: f1( f2( f3()))
, и в этом случае вы знаете это f3
будет оцениваться первым, потому что оценка f2
зависит от его значения. [Изменить: См. также комментарий ниже о ленивом и нетерпеливом оценивании. Это важно, потому что ленивое вычисление на самом деле довольно часто встречается в очень чистых контекстах; например, стандартная реализация рекурсии в чистом лямбда-исчислении не приводит к завершению при активной оценке.]
Опять же, чтобы написать интерактивную программу на основе лямбда-исчисления, вы, вероятно, сделали бы это именно так. Если вам нужно что-то действительно пригодное для программирования, вы, вероятно, захотите объединить часть композиции функций с концептуальной структурой функций, принимающих и возвращающих значения, представляющие состояние мира, и создать некоторую абстракцию более высокого порядка для обработки конвейерной обработки " Мировое состояние »значения между функциями ввода / вывода, в идеале также сохраняющее« мировое состояние », содержащееся для обеспечения строгой линейности - и в этот момент вы почти заново изобрели IO
Монаду Haskell.
Надеюсь, это не только запутало вас еще еще.
f2
и f1
фактически используют свой аргумент для вычисления своего результата, и только если вызывающий f1
также будет использовать его результат. В противном случае ленивое оценивание может законно сработать.
- person Pavel Minaev; 16.12.2009
Haskell - это чистый функциональный язык программирования. В Haskell все функции чистые (т.е. они всегда дают одинаковый результат для одних и тех же входов). Но как справиться с побочными эффектами в Haskell? Что ж, эта проблема прекрасно решается с помощью использования монад.
Возьмем в качестве примера ввод-вывод. В Haskell каждая функция, выполняющая ввод-вывод, возвращает вычисление ввода-вывода, то есть вычисление в монаде ввода-вывода. Так, например, функция, которая считывает int с клавиатуры, вместо того, чтобы возвращать int, возвращает вычисление ввода-вывода, которое дает int при запуске:
askForInt :: String -> IO Int
Поскольку он возвращает вычисление ввода-вывода вместо Int
, вы не можете использовать этот результат непосредственно в сумме, например. Чтобы получить доступ к значению Int
, вам необходимо «развернуть» вычисление. Единственный способ сделать это - использовать функцию привязки (>>=
):
(>>=) :: IO a -> (a -> IO b) -> IO b
Поскольку это также возвращает вычисление ввода-вывода, вы всегда получаете вычисление ввода-вывода. Вот как Haskell изолирует побочные эффекты. Монада ввода-вывода действует как абстракция состояния реального мира (фактически, внутри она обычно реализуется с помощью типа RealWorld
для части состояния).
unsafePerformIO
)
- person R. Martinho Fernandes; 16.12.2009
IO
), создает другое состояние мира (возвращенное IO
монада).
- person Pavel Minaev; 16.12.2009
Для взаимодействия с пользователем и удаленной службы требуется какая-то нефункциональная часть вашего программного обеспечения.
Многие «функциональные языки» (как и большинство Лиспов) не являются чисто функциональными. Они по-прежнему позволяют делать вещи с побочными эффектами, хотя побочные эффекты в большинстве случаев «не приветствуются».
Haskell «чисто функциональный», но все же позволяет делать нефункциональные вещи через монаду ввода-вывода. Основная идея состоит в том, что ваша чисто функциональная программа генерирует ленивую структуру данных, которая оценивается нефункциональной программой (которую вы не пишете, это часть среды). Кто-то может возразить, что эта структура данных сама по себе является императивной программой. Итак, вы выполняете императивное метапрограммирование на функциональном языке.
Игнорируя, какой подход «лучше», цель в обоих случаях состоит в том, чтобы разделить функциональную и нефункциональную части ваших программ и максимально ограничить размер нефункциональных частей. Функциональные части, как правило, более пригодны для многократного использования, их можно тестировать и о них легче рассуждать.
Функциональное программирование - это ограничение и изоляция побочных эффектов, а не попытки полностью избавиться от них ... потому что вы не можете.
... и да, я считаю FP полезным (в любом случае, с Erlang): я считаю, что легче перейти от "идеи" к "программе" (или от проблемы к решению;) ... но, конечно, это мог быть только я.
Единственный полностью чистый функциональный язык, о котором я знаю, - это система шаблонов в C ++. Haskell занимает второе место, поскольку делает императивные части программы явными.
В Haskell программа имеет изменяемое состояние, а функции (почти всегда) - нет. Вы сохраняете чистотой около 99% программы, и только та часть, которая взаимодействует с внешним миром, является нечистой. Поэтому, когда вы тестируете функцию, вы знаете, что побочных эффектов нет. Чистое ядро с нечистой оболочкой.
Вам необходимо знать хотя бы еще одну важную концепцию: Монады. Это понадобится вам для ввода-вывода и других "полезных" вещей!
Haskell делает это с помощью монад, см. wikipedia и объяснение Haskell. на их странице.
По сути, идея состоит в том, что вы не избавляетесь от монады ввода-вывода. Насколько я понимаю, вы можете связать функции, которые разворачивают монаду ввода-вывода, и выполнять эту функцию. Но вы не можете полностью удалить монаду ввода-вывода.
Другой пример использования монад, не связанных напрямую с вводом-выводом, - это монада Maybe. В отличие от монады ввода-вывода, эта монада «необразуема». Но проще объяснить использование монад с помощью монады 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-монадой.
>>=
), которая разворачивает вычисление ввода-вывода и передает его функции, которая возвращает другое вычисление ввода-вывода. Поскольку нет другого способа развернуть вычисление ввода-вывода, вы не можете избавиться от него.
- person R. Martinho Fernandes; 16.12.2009
Учитывая, что большинство программ оказывают влияние на внешний мир (запись в файлы, изменение данных в базе данных ...), программы в целом редко бывают свободными от побочных эффектов. Вне академических упражнений нет смысла даже пытаться.
Но программы собираются из строительных блоков (подпрограмма, функция, метод, называйте это как хотите), а чистые функции создают очень хорошо работающие строительные блоки.
Большинство языков функционального программирования не требуют, чтобы функции были чистыми, хотя хорошие функциональные программисты постараются сделать столько своих функций чистыми, насколько это возможно и практично, чтобы воспользоваться преимуществами ссылочной прозрачности.
Haskell идет дальше. Каждая часть программы на Haskell чиста (по крайней мере, в отсутствие таких ошибок, как "unsafePerformIO"). Все функции, которые вы пишете на Haskell, чисты.
Побочные эффекты вводятся через монады. Их можно использовать, чтобы ввести своего рода разделение «список покупок - покупатель». По сути, ваша программа составляет список покупок (который представляет собой просто данные, которыми можно манипулировать в чистом виде), в то время как среда выполнения языка интерпретирует список покупок и выполняет эффективные покупки. Весь ваш код чист и удобен для эквациональных рассуждений и тому подобного, тогда как нечистый код предоставляется разработчиками компилятора.
Даже если вы не используете его в своей работе, изучение одного или нескольких языков функционального программирования - отличный способ научиться думать по-другому и дает вам инструментарий альтернативных подходов к проблемам (это также может расстроить вас, когда вы не можете сделать что-то столь же аккуратное и чистое, как функциональный подход на других языках).
И это помогло мне лучше писать таблицы стилей XSL.