Почему побочные эффекты считаются злыми в функциональном программировании?


69

Я чувствую, что побочные эффекты - это естественное явление. Но это что-то вроде табу в функциональных языках. Каковы причины?

Мой вопрос относится к стилю функционального программирования. Не все языки программирования / парадигмы.


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

@JacquesB Было бы хорошим ответом объяснить, почему их легче понять, легче анализировать, легче тестировать и легче оптимизировать.
ceving

Ответы:


72

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

Это также облегчает создание этих функций для создания нового поведения.

Это также делает возможной определенную оптимизацию, когда компилятор может, например, запоминать результаты функций или использовать Common Subexpression Elission.

Редактировать: по запросу Бенджола: Поскольку большая часть вашего состояния хранится в стеке (поток данных, а не поток управления, как его здесь назвал Джонас ), вы можете распараллелить или иным образом изменить порядок выполнения тех частей вашего вычисления, которые не зависят от друг с другом. Вы можете легко найти эти независимые части, потому что одна часть не обеспечивает входы для другой.

В средах с отладчиками, которые позволяют вам откатить стек и возобновить вычисления (например, Smalltalk), наличие чистых функций означает, что вы очень легко сможете увидеть, как изменяется значение, поскольку предыдущие состояния доступны для проверки. В вычислениях с высокой мутацией, если вы явно не добавите действия do / undo в свою структуру или алгоритм, вы не сможете увидеть историю вычислений. (Это связано с первым абзацем: написание чистых функций облегчает проверку правильности вашей программы.)


4
Может быть, стоит добавить что-нибудь о параллелизме в своем ответе?
Бенджол

5
Функции без побочных эффектов легче тестировать и использовать повторно.
LennyProgrammers

@ Lenny222: повторное использование было тем, на что я намекал, говоря о композиции функций.
Фрэнк Шиарар

@Frank: Ах, хорошо, слишком поверхностный просмотр. :)
LennyProgrammers

@ Lenny222: все в порядке; это, наверное, хорошая вещь, чтобы объяснить это.
Фрэнк Шиарар

23

Из статьи о функциональном программировании :

На практике приложения должны иметь некоторые побочные эффекты. Саймон Пейтон-Джонс, основной разработчик языка функционального программирования Haskell, сказал следующее: «В конце концов, любая программа должна манипулировать состоянием. Программа, которая не имеет побочных эффектов, является своего рода черным ящиком. Все, что вы можете сказать, это что коробка становится горячее. " ( http://oscon.blip.tv/file/324976 ) Ключ заключается в том, чтобы ограничить побочные эффекты, четко идентифицировать их и избегать рассеивания их по всему коду.



23

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

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


Вот почему они «что-то вроде табу» - FPL поощряют вас ограничивать побочные эффекты.
Фрэнк Шиарар

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

Для пояснения, я не сказал «почему побочный эффект недопустим в функциональном программировании» или «почему побочный эффект не нужен». Я знаю, что это разрешено на функциональных языках и иногда является обязательным. Но это очень не рекомендуется в функциональном программировании. Почему? Это был мой вопрос.
Гульшан

@Gulshan - потому что побочные эффекты затрудняют понимание и оптимизацию программ.
ChaosPandion

в случае с haskell главное не ограничивать побочные эффекты. побочные эффекты невозможно выразить в ЯЗЫКЕ. то, что делают функции readFile, это определение последовательности действий. эта последовательность функционально чиста и похожа на абстрактное дерево, описывающее ЧТО делать. фактические грязные побочные эффекты затем выполняются во время выполнения.
Сара

13

Несколько заметок:

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

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


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

1
@ NoelWidmer Нечто подобное уже существует. В Oracle PL / SQL есть deterministicпредложение для функций без побочных эффектов, поэтому они не выполняются чаще, чем необходимо.
user281377

Вау! Тем не менее, я думаю, что языки должны быть семантически выразительными, чтобы компилятор мог сам разобраться в этом без необходимости указывать явный флаг (я не уверен, что такое предложение). Решением может быть указание параметров, которые должны быть изменяемыми / неизменяемыми, например, в общем случае это потребовало бы системы строгого типа, в которой компилятор мог бы делать предположения о побочных эффектах. И эту функцию нужно будет отключить при желании. Отказаться от участия. Это только мое мнение, основанное на ограниченных знаниях, которые у меня есть с тех пор, как я прочитал ваш ответ :)
Ноэль Видмер

Предложение deterministic- это просто ключевое слово, которое сообщает компилятору, что это детерминированная функция, сравнимо с тем, как finalключевое слово в Java сообщает компилятору, что переменная не может быть изменена.
user281377

11

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

Рассмотрим этот простой пример:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

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

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


6

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

Почему побочные эффекты считаются злом?

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

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

Выгоды

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

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

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


1
Ада очень затрудняет возникновение побочных эффектов. Это не невозможно, но вы четко знаете, что делаете тогда.
Mouviciel

@mouviciel: Я думаю, что есть, по крайней мере, несколько полезных языков, которые сильно затрудняют побочные эффекты, и стараются передать их Монадам.
Мерлин Морган-Грэм

4

Побочные эффекты похожи на «утечки» в вашем коде, которые нужно будет обработать позже, либо вам, либо не подозревающему коллеге.

Функциональные языки избегают переменных состояния и изменяемых данных как способа сделать код менее зависимым от контекста и более модульным. Модульность гарантирует, что работа одного разработчика не повлияет / не подорвет работу другого.

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


+1 - «или какой-то ничего не подозревающий сотрудник»
Мерлин Морган-Грэм

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

Этот комментарий появился шесть лет спустя, но есть побочные эффекты, а затем есть побочные эффекты. Желаемый вид побочных эффектов, выполнение операций ввода-вывода и т. Д. Действительно необходимы любой программе, потому что вам нужно каким-то образом предоставлять свои результаты пользователю - но другой вид побочных эффектов, когда ваш код изменяет состояние без хорошего Такие причины, как ввод-вывод, действительно являются «утечкой», которая должна быть устранена позже. Основная идея - разделение команд и запросов : функция, которая возвращает значение, не должна иметь побочного эффекта.
Рунн

4

Ну, ИМХО, это довольно лицемерно. Никто не любит побочные эффекты, но они нужны всем.

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

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

Функциональное программирование использует гораздо более радикальный подход, когда состояние приложения просто неизменно с точки зрения программиста. Это хорошая идея, но делает язык бесполезным сам по себе. Почему? Потому что у ЛЮБОЙ операции ввода / вывода есть побочные эффекты. Как только вы читаете из любого входного потока, состояние вашего приложения, вероятно, изменится, потому что в следующий раз, когда вы вызовете ту же функцию, результат, скорее всего, будет другим. Возможно, вы читаете разные данные, или, возможно, операция не удалась. То же самое верно для вывода. Ровный вывод - это операция с побочными эффектами. В наши дни это часто не осознают, но представьте, что у вас есть только 20 КБ для вывода, и если вы выводите больше, ваше приложение падает, потому что у вас недостаточно места на диске или что-то еще.

Так что да, побочные эффекты неприятны и опасны с точки зрения программиста. Большинство ошибок происходит из-за того, что определенные части состояния приложения блокируются почти неясным способом, через необдуманные и часто ненужные побочные эффекты. С точки зрения пользователя, побочные эффекты - это смысл использования компьютера. Их не волнует, что происходит внутри или как это организовано. Они что-то делают и ожидают, что компьютер изменится соответственно.


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

@ Илан: Это также верно для некоторых функциональных языков, и этот стиль легко принять.
back2dos

«Функциональное программирование использует гораздо более радикальный подход, когда состояние приложения просто неизменно с точки зрения программиста. Это хорошая идея, но делает язык бесполезным сам по себе. Почему? Потому что у ЛЮБОЙ операции ввода / вывода есть сторона эффекты »: FP не запрещает побочные эффекты, а ограничивает их, когда это не нужно. Например (1) I / O -> побочные эффекты необходимы; (2) вычисление агрегатной функции из последовательности значений -> побочный эффект (например, для цикла с переменной аккумулятора) не требуется.
Джорджио

2

Любой побочный эффект вводит дополнительные параметры ввода / вывода, которые необходимо учитывать при тестировании.

Это делает проверку кода намного более сложной, поскольку среда не может быть ограничена только проверяемым кодом, но должна включать в себя некоторую или всю окружающую среду (обновленная глобальная часть живет в этом коде, что, в свою очередь, зависит от этого). код, который, в свою очередь, зависит от жизни внутри полноценного сервера Java EE ....)

Стараясь избегать побочных эффектов, вы ограничиваете количество экстернализма, необходимого для запуска кода.


1

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

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

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

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


1
Наблюдатели (включая пользовательский интерфейс) должны узнать об изменениях, подписавшись на сообщения / события, которые отправляет ваш объект. Это не является побочным эффектом, если объект непосредственно не изменяет наблюдателя - что было бы плохим дизайном.
ChrisF

1
@ChrisF Наиболее определенно это побочный эффект. Сообщение, передаваемое наблюдателю (в языке OO, скорее всего, вызов метода на интерфейсе) приведет к состоянию компонента UI при изменении кучи (и эти объекты кучи видны другим частям программы). Компонент пользовательского интерфейса не является ни параметром метода, ни возвращаемым значением. В формальном смысле, чтобы функция не имела побочных эффектов, она должна быть идемпотентной. Уведомление в шаблоне MVC отсутствует, например, пользовательский интерфейс может отображать список сообщений, которые он получил - консоль - при его повторном вызове это приводит к другому состоянию программы.
пламенеющий пингвин

0

Зло немного выше всяких проблем ... все зависит от контекста использования языка.

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


0

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

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

  • В коде с изменяемым состоянием мы можем управлять областью действия состояния таким образом, чтобы статически гарантировать, что оно не может просочиться за пределы заданной функции, что позволяет нам собирать мусор без схем подсчета ссылок или стиля метки и развертки И все же будьте уверены, что ни одна из ссылок не выживет. Те же самые гарантии также полезны для хранения конфиденциальной информации и т. Д. (Это может быть достигнуто с помощью монады ST в haskell)
  • При изменении общего состояния в нескольких потоках мы можем избежать необходимости блокировок, отслеживая изменения и выполняя атомарное обновление в конце транзакции или откатывая транзакцию и повторяя ее, если другой поток произвел конфликтующую модификацию. Это достижимо только потому, что мы можем гарантировать, что код не имеет никаких эффектов, кроме изменений состояния (от которых мы можем с радостью отказаться). Это выполняется монадой STM (Software Transactional Memory) в Haskell.
  • мы можем отслеживать эффекты кода и тривиально помещать его в песочницу, отфильтровывая любые эффекты, которые ему могут понадобиться, чтобы быть уверенными в его безопасности, что позволяет (например) вводить код, введенный пользователем, безопасно на веб-сайте.

0

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

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

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

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

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

Поэтому, когда у меня есть такие случаи, мне часто бывает очень трудно, если не невозможно, чувствовать себя очень уверенным в правильности такого кода, не говоря уже о том, что я могу внести изменения в такой код, не наткнувшись на что-то неожиданное. Таким образом, для меня решение состоит в том, чтобы либо упростить поток управления, либо минимизировать / унифицировать побочные эффекты (под объединением я подразумеваю, как вызывающий побочный эффект только одного типа для многих вещей в течение определенной фазы в системе, а не для двух или трех или дюжина). Мне нужно, чтобы произошло одно из этих двух событий, чтобы мой простой мозг чувствовал себя уверенно относительно правильности существующего кода и правильности внесенных мною изменений. Довольно легко быть уверенным в правильности кода, вводящего побочные эффекты, если побочные эффекты единообразны и просты вместе с потоком управления, например, так:

for each pixel in an image:
    make it red

Правильность такого кода довольно легко рассуждать, но в основном потому, что побочные эффекты настолько однородны, а поток управления настолько прост. Но допустим, у нас был такой код:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove edge
         remove vertex

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

for each vertex to remove:
     mark connected edges
for each marked edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked edge:
     remove edge
for each vertex to remove:
     remove vertex

... что-то на этот счет как одна итерация упрощения. Это означает, что мы пропускаем данные несколько раз, что, безусловно, влечет за собой вычислительные затраты, но мы часто обнаруживаем, что можем упростить многопоточность такого получающегося в результате кода, теперь, когда побочные эффекты и потоки управления приобрели такую ​​однородную и более простую природу. Кроме того, каждый цикл можно сделать более удобным для кэширования, чем обход связанного графа и вызывающий побочные эффекты по ходу дела (например: используйте параллельный бит, установленный для отметки того, что необходимо пройти, чтобы мы могли затем выполнять отсроченные проходы в отсортированном последовательном порядке используя битовые маски и FFS). Но самое главное, я считаю, что вторую версию гораздо проще рассуждать с точки зрения правильности, а также изменений, не вызывая ошибок. Так что'

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


-1

Это не зло. На мой взгляд, необходимо различать два типа функций - с побочными эффектами и без. Функция без побочных эффектов: - всегда возвращает одно и то же с одинаковыми аргументами, поэтому, например, такая функция без каких-либо аргументов не имеет смысла. - Это также означает, что порядок, в котором некоторые такие функции называются, не играет роли - должен быть в состоянии работать и может быть отлажен только один (!), Без какого-либо другого кода. А теперь послушай, что делает JUnit. Функция с побочными эффектами: - имеет своего рода «утечки», что может быть выделено автоматически - это очень важно при отладке и поиске ошибок, которые обычно вызваны побочными эффектами. - Любая функция с побочными эффектами также имеет «часть» сама по себе без побочных эффектов, которую также можно разделить автоматически. Так злые эти побочные эффекты,


кажется, это не дает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 12 ответах
комнат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.