Возможны ли побочные эффекты в чисто функциональном программировании

Я уже давно пытаюсь осмыслить функциональное программирование? Я искал лямбда-исчисление, LISP, OCML, F # и даже комбинаторную логику, но основная проблема, с которой я столкнулся, заключается в том, как вы делаете то, что требует побочных эффектов, таких как (взаимодействие с пользователем, общение с удаленной службой или даже обработка моделирования с использованием случайная выборка), не нарушая фундаментальную предпосылку чистого функционального программирования, заключающуюся в том, что для заданного входа выход является детерминированным? Я надеюсь, что у меня есть смысл, иначе я приветствую любые попытки меня должным образом обучить. Заранее спасибо.


person Jeremy E    schedule 16.12.2009    source источник
comment
Кстати, заголовок вашего вопроса и вопрос очень разные. Вы можете изменить заголовок на Возможны ли побочные эффекты на чисто функциональном языке?   -  person Chip Uni    schedule 16.12.2009
comment
Связано: stackoverflow.com/questions/17002119/haskell-pre-monadic- io.   -  person atravers    schedule 17.11.2020


Ответы (9)


Большая часть реального функционального программирования не является «чистым» во многих смыслах, поэтому половина ответа на ваш вопрос - «вы делаете это, отказываясь от чистоты». Тем не менее, есть альтернативы.

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

Аналогичная перспектива состоит в том, чтобы объявить, что входные данные функции - это «все состояние внешнего мира» и что оценка функции возвращает новое, измененное «состояние мира». В этом случае любая функция в программе, которая использует состояние мира, очевидно, свободна от «детерминированности», поскольку никакие две оценки программы не будут иметь абсолютно одинаковый внешний мир.

Если бы вы хотели написать интерактивную программу на чистом лямбда-исчислении (или что-то подобное, например, на эзотерическом языке Lazy K), концептуально вы бы это сделали.

В более практическом плане проблема сводится к тому, чтобы убедиться, что ввод-вывод происходит в правильном порядке, когда ввод используется в качестве аргумента функции. Общая структура «чистого» решения этой проблемы - это композиция функций. Например, предположим, что у вас есть три функции, которые выполняют ввод-вывод, и вы хотите вызывать их в определенном порядке. Если вы сделаете что-то вроде RunThreeFunctions(f1, f2, f3), нет ничего, чтобы определить порядок, в котором они будут оцениваться. С другой стороны, если вы позволите каждой функции принимать другую функцию в качестве аргумента, вы можете связать их следующим образом: f1( f2( f3())), и в этом случае вы знаете это f3 будет оцениваться первым, потому что оценка f2 зависит от его значения. [Изменить: См. также комментарий ниже о ленивом и нетерпеливом оценивании. Это важно, потому что ленивое вычисление на самом деле довольно часто встречается в очень чистых контекстах; например, стандартная реализация рекурсии в чистом лямбда-исчислении не приводит к завершению при активной оценке.]

Опять же, чтобы написать интерактивную программу на основе лямбда-исчисления, вы, вероятно, сделали бы это именно так. Если вам нужно что-то действительно пригодное для программирования, вы, вероятно, захотите объединить часть композиции функций с концептуальной структурой функций, принимающих и возвращающих значения, представляющие состояние мира, и создать некоторую абстракцию более высокого порядка для обработки конвейерной обработки " Мировое состояние »значения между функциями ввода / вывода, в идеале также сохраняющее« мировое состояние », содержащееся для обеспечения строгой линейности - и в этот момент вы почти заново изобрели IO Монаду Haskell.

Надеюсь, это не только запутало вас еще еще.

person C. A. McCann    schedule 16.12.2009
comment
С другой стороны, если вы позволите каждой функции принимать другую функцию в качестве аргумента, вы можете связать их следующим образом: f1 (f2 (f3 ())), и в этом случае вы знаете, что f3 будет оцениваться первым, потому что оценка f2 зависит от его стоимости. - только если f2 и f1 фактически используют свой аргумент для вычисления своего результата, и только если вызывающий f1 также будет использовать его результат. В противном случае ленивое оценивание может законно сработать. - person Pavel Minaev; 16.12.2009
comment
Вы, конечно, правы, и я упустил много других деталей, чтобы не усугубить и без того потрясающую многословность ответа. - person C. A. McCann; 16.12.2009
comment
Итак, меняется ли состояние мира по мере оценки функций? Что переписывает мировую государственную структуру вне приложения? Вам нужно постоянно вызывать свое приложение в цикле, чтобы иметь дело с изменяющимся мировым состоянием? - person Jeremy E; 16.12.2009
comment
Мировое состояние - это в основном концептуальная абстракция, а не фактическая структура данных в программе. Важно только то, что любая функция с побочными эффектами принимает состояние мира в качестве параметра и по завершении возвращает новое состояние мира. Последовательность уникальных токенов состояния мира по мере их передачи между функциями представляет безвозвратные эффекты, которые программа оказывает на внешний мир. До тех пор, пока состояния мира никогда не будут использоваться повторно, будет сохраняться согласованная структура. - person C. A. McCann; 16.12.2009
comment
Что касается вызова приложения в цикле, я не понимаю, что вы имеете в виду. Грубо говоря, приложение - это единственная (сложная) функция мирового состояния; внутри приложения находятся другие функции состояния мира. Когда программа запускается, одна за другой создается целая группа состояний внутреннего мира, причем конечным состоянием является то, которое возвращается приложением в целом. - person C. A. McCann; 16.12.2009
comment
Итак, позвольте мне уточнить, например, состоянием мира для шахматной игры будет доска и последовательность ходов, сгенерированная для перевода доски в текущее состояние, монады инкапсулируют выбор отдельных игроков относительно того, какой ход они решили сделать. , это правильный пример? - person Jeremy E; 16.12.2009
comment
Думайте о мировом состоянии как об абстракции среды приложения: память, диск, экран, экран удаленных компьютеров и т. Д. Ваше приложение принимает их коллекцию в качестве входных данных и создает другую коллекцию в качестве выходных. Пока разные состояния мира нельзя использовать повторно, это нормально. Ваш пример шахмат - это пример монады состояния, состояние которой - доска. Монада инкапсулирует последовательность изменений этого состояния. Однако эти состояния можно использовать повторно (подумайте об отмене), в то время как абстракция состояния мира - нет. Такого рода побочные эффекты необратимы. - person R. Martinho Fernandes; 16.12.2009
comment
Чтобы быть педантичным, мировое состояние для шахматной игры также должно включать, чей это ход, и частичную историю перемещений определенных фигур (из-за правил для прохода взятия и рокировки). Монада на основе состояний в стиле Haskell инкапсулирует детали этого состояния и абстрагирует процесс передачи данных состояния между функциями. Теоретически вы можете передать явную структуру состояния для того же эффекта - монада существует только для обеспечения правильности и избегания шаблонного кода. - person C. A. McCann; 17.12.2009

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

Возьмем в качестве примера ввод-вывод. В Haskell каждая функция, выполняющая ввод-вывод, возвращает вычисление ввода-вывода, то есть вычисление в монаде ввода-вывода. Так, например, функция, которая считывает int с клавиатуры, вместо того, чтобы возвращать int, возвращает вычисление ввода-вывода, которое дает int при запуске:

askForInt :: String -> IO Int

Поскольку он возвращает вычисление ввода-вывода вместо Int, вы не можете использовать этот результат непосредственно в сумме, например. Чтобы получить доступ к значению Int, вам необходимо «развернуть» вычисление. Единственный способ сделать это - использовать функцию привязки (>>=):

(>>=) :: IO a -> (a -> IO b) -> IO b

Поскольку это также возвращает вычисление ввода-вывода, вы всегда получаете вычисление ввода-вывода. Вот как Haskell изолирует побочные эффекты. Монада ввода-вывода действует как абстракция состояния реального мира (фактически, внутри она обычно реализуется с помощью типа RealWorld для части состояния).

person R. Martinho Fernandes    schedule 16.12.2009
comment
Но поскольку монады не являются функциональными, Haskell не является чисто функциональным языком программирования. - person RBarryYoung; 16.12.2009
comment
@RBarryYoung: что ты имеешь в виду, они не работают? Почему вы говорите, что Haskell не является чисто функциональным? Haskell является чисто функциональным. - person Stephan202; 16.12.2009
comment
Чистый функциональный язык не означает, что все является функцией. Это функциональный язык, и все функции чистые (не говоря уже о unsafePerformIO) - person R. Martinho Fernandes; 16.12.2009
comment
Haskell чист. Программы на Haskell не имеют побочных эффектов - они возвращают вычисление (не значение), которое, учитывая одно состояние мира (заключенное в монаду IO), создает другое состояние мира (возвращенное IO монада). - person Pavel Minaev; 16.12.2009
comment
В этом сообщении в блоге (не мной) содержится интересный / юмористический аргумент в пользу того, что C является чисто функциональным: conal.net/blog/posts/the-c-language-is-purely-functional - person Laurence Gonsalves; 17.12.2009
comment
@RBarryYoung Это все равно что сказать, что это не чисто функционально, потому что в нем есть целые числа. - person Jeremy List; 31.03.2014

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

Многие «функциональные языки» (как и большинство Лиспов) не являются чисто функциональными. Они по-прежнему позволяют делать вещи с побочными эффектами, хотя побочные эффекты в большинстве случаев «не приветствуются».

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

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

person Laurence Gonsalves    schedule 16.12.2009

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

... и да, я считаю FP полезным (в любом случае, с Erlang): я считаю, что легче перейти от "идеи" к "программе" (или от проблемы к решению;) ... но, конечно, это мог быть только я.

person jldupont    schedule 16.12.2009
comment
Я тоже, мне всегда нравились концепции конвейерной обработки и фильтрации как основных функций обработки данных, что является фундаментальным принципом функционального программирования в целом. - person Jeremy E; 16.12.2009
comment
@Jeremy E: согласен: я пришел из инженерного фона HW и очень часто думаю о потоках данных, как и позволяет мне Erlang. - person jldupont; 21.12.2009

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

В Haskell программа имеет изменяемое состояние, а функции (почти всегда) - нет. Вы сохраняете чистотой около 99% программы, и только та часть, которая взаимодействует с внешним миром, является нечистой. Поэтому, когда вы тестируете функцию, вы знаете, что побочных эффектов нет. Чистое ядро ​​с нечистой оболочкой.

person Jonathan Fischoff    schedule 16.12.2009
comment
Я начал приходить к тому же самому выводу, каждый помогал мне прояснить это. - person Jeremy E; 16.12.2009
comment
В Haskell нет изменяемого состояния: он подделывает его, используя определение newtype StateT s m a = StateT {runStateT :: s - ›m (a, s)}, а IO имеет почти идентичное определение. - person Jeremy List; 31.03.2014

Вам необходимо знать хотя бы еще одну важную концепцию: Монады. Это понадобится вам для ввода-вывода и других "полезных" вещей!

person Agos    schedule 16.12.2009

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 Ruben    schedule 16.12.2009
comment
Это идея. Есть функция / оператор связывания (>>=), которая разворачивает вычисление ввода-вывода и передает его функции, которая возвращает другое вычисление ввода-вывода. Поскольку нет другого способа развернуть вычисление ввода-вывода, вы не можете избавиться от него. - person R. Martinho Fernandes; 16.12.2009
comment
@Ruben: 'unwrappable' = ›Для дальнейшего использования эти монады называются открытыми монадами, в отличие от IO, являющегося закрытой монадой. - person R. Martinho Fernandes; 16.12.2009

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

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

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

Haskell идет дальше. Каждая часть программы на Haskell чиста (по крайней мере, в отсутствие таких ошибок, как "unsafePerformIO"). Все функции, которые вы пишете на Haskell, чисты.

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

person Waquo    schedule 20.12.2009

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

И это помогло мне лучше писать таблицы стилей XSL.

person Tom    schedule 16.12.2009
comment
XSLT: Потому что полный по Тьюрингу XML - это именно то, что нужно миру. - person C. A. McCann; 16.12.2009
comment
@camccann Надеюсь, ты саркастичен. - person R. Martinho Fernandes; 16.12.2009
comment
Я считаю, что полный по Тьюрингу XML намного предпочтительнее, чем полный по Тьюрингу механизм обобщения типов (например, шаблоны C ++ и т. Д.). Тем не менее, для ненавистников угловых скобок есть XQuery. - person Pavel Minaev; 16.12.2009
comment
Саркастический о мире, нуждающемся в полном по Тьюрингу XML: Да. Саркастический о XSLT том, что полный по Тьюрингу: К сожалению, нет. (И даже не упоминайте языки набора или системы сборки с полным набором по Тьюрингу.) - person C. A. McCann; 16.12.2009
comment
@Pavel Minaev: С другой стороны, возвращаясь к теме, шаблоны C ++ на самом деле являются чисто функциональным языком! - person C. A. McCann; 16.12.2009