Побочные эффекты - это хорошо?

Я считаю этот термин уничижительным. Следовательно, я ошеломлен двумя предложениями в Википедии:

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

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


person Léo Léopold Hertz 준영    schedule 18.04.2009    source источник


Ответы (13)


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

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

Побочные эффекты необходимы для того, чтобы программа могла взаимодействовать с внешним миром (людьми, файловыми системами, другими компьютерами в сети). Но степень использования побочных эффектов зависит от парадигмы программирования. Императивное программирование известно неконтролируемым беспорядочным использованием побочных эффектов. В функциональном программировании побочные эффекты используются редко. Функциональные языки, такие как Standard ML и Scheme, не ограничивают побочные эффекты, но программисты обычно их избегают. Функциональный язык Haskell ограничивает побочные эффекты с помощью системы статических типов; только функция, которая производит результат типа IO, может иметь побочные эффекты.

person Norman Ramsey    schedule 20.04.2009

Побочные эффекты - неизбежное зло, и их следует минимизировать / локализовать.

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

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

person Brian    schedule 18.04.2009
comment
Программирование без эффектов может быть таким же эффективным, как и эффективное программирование (см. Mlton.org или caml.inria.fr), но верно то, что разработчикам компиляторов необходимо усерднее работать, чтобы добиться этого. - person Norman Ramsey; 20.04.2009

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

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

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

person mmx    schedule 18.04.2009
comment
Программы без побочных эффектов - это нехорошо. Все программы без побочных эффектов можно полностью оптимизировать. - person David Conrad; 17.12.2012

Pro:

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

Против:

  • Чистый код легко распараллелить.
  • Побочные эффекты могут усложнить код.
  • Чистый код легче доказать.

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

person John Smith    schedule 18.04.2009
comment
Побочные эффекты делают алгоритмы простыми, потому что они очень мощные, а мощные инструменты следует использовать с осторожностью. - person Anton Tykhyy; 19.04.2009
comment
Для циклов компилятору нужно оптимизировать только хвостовые рекурсии, а не хвостовые вызовы в целом, и это слишком просто. - person Ingo; 20.04.2009
comment
Я думаю, что меньшее или большее модульное более ясно и конкретно, чем сложное. - person Kzqai; 14.02.2010

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

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

person Pillsy    schedule 18.04.2009

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

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

В C ++ побочные эффекты полностью неограничены благодаря указателям. Если переменная объявлена ​​как «частная», вы все равно можете получить к ней доступ или изменить ее, используя уловки с указателями. Вы даже можете изменять переменные, которые не входят в область видимости, например параметры и локальные переменные вызывающей функции. С небольшой помощью ОС (mmap) вы даже можете изменить машинный код вашей программы во время выполнения! Когда вы пишете на таком языке, как C ++, вы поднимаетесь до ранга Bit God, хозяина всей памяти в вашем процессе. Все оптимизации, которые компилятор вносит в ваш код, сделаны с предположением, что вы не злоупотребляете своими полномочиями.

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

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

Миранда, предшественник Haskell, представляет собой чистый функциональный язык, созданный до того, как монады стали популярными. Миранда (насколько я знаю ... если я ошибаюсь, замените Lambda Calculus) вообще не имеет примитивов ввода-вывода. Единственный выполняемый ввод-вывод - это компиляция программы (ввод), запуск программы и печать результата (вывода). Здесь у вас полная чистота. Порядок исполнения значения не имеет. Все «эффекты» являются локальными по отношению к функциям, которые их объявляют, что означает, что две непересекающиеся части кода никогда не могут влиять друг на друга. Это утопия (для математиков). Или, что то же самое, distpia. Это скучно. Ничего не происходит. Вы не можете написать для него сервер. На нем нельзя написать ОС. В нем нельзя написать ЗМЕЮ или Тетрис. Все просто сидят и смотрят математики.

person Tac-Tics    schedule 26.04.2010
comment
Извините, что возвращаюсь к ответу 10-летней давности, но разве вы не имеете в виду антиутопию? - person ARI FISHER; 27.11.2020

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

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

Это все хорошо =) Вы должны использовать разные парадигмы в зависимости от решаемой проблемы.

person bigmonachus    schedule 18.04.2009

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

Рассмотрим следующую аналогию: процессор с набором инструкций без инструкций ветвления будет абсолютно бесполезным. Однако из этого не следует, что программисты должны использовать goto все время. Напротив, оказалось, что структурированное программирование и более поздние языки ООП, такие как Java, могут обойтись даже без оператора goto, и никто не пропустил его.

(Конечно, в Java все еще есть goto - теперь он называется break, continue и throw.)

person Ingo    schedule 20.04.2009
comment
if, else, while и for - это столько же gotos, сколько break, continue и throw. - person hasen; 20.04.2009

Без побочных эффектов вы не можете выполнять операции ввода-вывода; так что вы не сможете сделать полезное приложение.

person hasen    schedule 18.04.2009

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

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

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

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

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

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

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

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

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

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

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

person Community    schedule 03.01.2018

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

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

person lex82    schedule 17.06.2015

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

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

person Promit    schedule 18.04.2009
comment
На самом деле, хотя найти программистов, владеющих функциональным языком, труднее, многие люди утверждают, что найти отличных программистов на самом деле проще. - person Magnus Kronqvist; 20.06.2011

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

Здесь я привожу две версии некоторого кода. Когда я начал искать возможности изменить код, похожий на первую версию, на код, похожий на вторую версию, моя жизнь стала лучше:

У него может быть объект с атрибутами first, second, third и методами first_method, second_method, third_method, например

def first_method(self):
    # calculate and set self.first

def second_method(self):
    # use self.first to calculate and set self.second

def third_method(self):
    # use self.first and self.second to calculate self.third

а затем метод более высокого уровня:

def method(self):
    self.first_method()
    self.second_method()
    self.third_method()

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

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

def first_func():
    # calculate the first thing and return it

def second_func(first_thing):
    # use first_thing to calculate second_thing and return it

def third_func(first_thing, second_thing):
    # use first_thing and second_thing to calculate third_thing and return it

и этот метод более высокого уровня может выглядеть как

def method(self):
    self.first_thing = first_func()
    self.second_thing = second_func(self.first_thing)
    self.third_thing = third_func(self.first_thing, self.second_thing)

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

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

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

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

person Philippe Carphin    schedule 09.07.2020