Я чувствую, что побочные эффекты - это естественное явление. Но это что-то вроде табу в функциональных языках. Каковы причины?
Мой вопрос относится к стилю функционального программирования. Не все языки программирования / парадигмы.
Я чувствую, что побочные эффекты - это естественное явление. Но это что-то вроде табу в функциональных языках. Каковы причины?
Мой вопрос относится к стилю функционального программирования. Не все языки программирования / парадигмы.
Ответы:
Написание ваших функций / методов без побочных эффектов - так что они являются чистыми функциями - облегчает рассуждения о правильности вашей программы.
Это также облегчает создание этих функций для создания нового поведения.
Это также делает возможной определенную оптимизацию, когда компилятор может, например, запоминать результаты функций или использовать Common Subexpression Elission.
Редактировать: по запросу Бенджола: Поскольку большая часть вашего состояния хранится в стеке (поток данных, а не поток управления, как его здесь назвал Джонас ), вы можете распараллелить или иным образом изменить порядок выполнения тех частей вашего вычисления, которые не зависят от друг с другом. Вы можете легко найти эти независимые части, потому что одна часть не обеспечивает входы для другой.
В средах с отладчиками, которые позволяют вам откатить стек и возобновить вычисления (например, Smalltalk), наличие чистых функций означает, что вы очень легко сможете увидеть, как изменяется значение, поскольку предыдущие состояния доступны для проверки. В вычислениях с высокой мутацией, если вы явно не добавите действия do / undo в свою структуру или алгоритм, вы не сможете увидеть историю вычислений. (Это связано с первым абзацем: написание чистых функций облегчает проверку правильности вашей программы.)
Из статьи о функциональном программировании :
На практике приложения должны иметь некоторые побочные эффекты. Саймон Пейтон-Джонс, основной разработчик языка функционального программирования Haskell, сказал следующее: «В конце концов, любая программа должна манипулировать состоянием. Программа, которая не имеет побочных эффектов, является своего рода черным ящиком. Все, что вы можете сказать, это что коробка становится горячее. " ( http://oscon.blip.tv/file/324976 ) Ключ заключается в том, чтобы ограничить побочные эффекты, четко идентифицировать их и избегать рассеивания их по всему коду.
Вы ошибаетесь, функциональное программирование способствует ограничению побочных эффектов, что облегчает понимание и оптимизацию программ. Даже Haskell позволяет писать в файлы.
По сути, я говорю, что функциональные программисты не думают, что побочные эффекты - это зло, они просто думают, что ограничение использования побочных эффектов - это хорошо. Я знаю, что это может показаться таким простым различием, но оно имеет все значение.
readFile
, это определение последовательности действий. эта последовательность функционально чиста и похожа на абстрактное дерево, описывающее ЧТО делать. фактические грязные побочные эффекты затем выполняются во время выполнения.
Несколько заметок:
Функции без побочных эффектов могут тривиально выполняться параллельно, тогда как функции с побочными эффектами обычно требуют некоторой синхронизации.
Функции без побочных эффектов допускают более агрессивную оптимизацию (например, путем прозрачного использования кэша результатов), потому что, пока мы получаем правильный результат, даже не имеет значения, была ли функция действительно выполнена
deterministic
предложение для функций без побочных эффектов, поэтому они не выполняются чаще, чем необходимо.
deterministic
- это просто ключевое слово, которое сообщает компилятору, что это детерминированная функция, сравнимо с тем, как final
ключевое слово в Java сообщает компилятору, что переменная не может быть изменена.
Сейчас я в основном работаю в функциональном коде, и с этой точки зрения это кажется ослепительно очевидным. Побочные эффекты создают огромную умственную нагрузку для программистов, пытающихся читать и понимать код. Вы не замечаете этого бремени, пока некоторое время не освобождаетесь от него, а затем вдруг снова приходится читать код с побочными эффектами.
Рассмотрим этот простой пример:
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. Мне даже не нужно смотреть на промежуточный код, тем более понимать его или смотреть на реализации функций, которые он вызывает.
Весь этот материал о параллелизме, распараллеливании и оптимизации хорош, но это то, что компьютерные ученые помещают в брошюру. Не нужно удивляться, кто изменяет вашу переменную и когда это то, чем я действительно наслаждаюсь в повседневной практике.
Немногие языки делают невозможным появление побочных эффектов. Языки, которые полностью свободны от побочных эффектов, будут чрезвычайно трудными (почти невозможными) для использования, за исключением очень ограниченных возможностей.
Почему побочные эффекты считаются злом?
Потому что они значительно затрудняют рассуждения о том, что именно делает программа, и доказывают, что она делает то, что от нее ожидают.
На очень высоком уровне представьте себе тестирование всего 3-уровневого веб-сайта с использованием только черного ящика. Конечно, это выполнимо, в зависимости от масштаба. Но, безусловно, происходит много дублирования. И если это ошибка , (что связано с побочным эффектом), то вы могли бы потенциально нарушить всю систему для дальнейшего тестирования, пока ошибка не будет поставлен диагноз и фиксированной, и исправление развертывается в тестовой среде.
Выгоды
Теперь уменьшите это. Если бы вы были достаточно хороши в написании кода без побочных эффектов, насколько быстрее вы бы рассуждали о том, что сделал какой-то существующий код? Насколько быстрее вы могли бы написать модульные тесты? Насколько уверенно вы чувствуете , что код без каких - либо побочных эффектов был гарантирован ошибкой бесплатно, и что пользователи могут ограничить их воздействие на любые ошибки это было есть?
Если у кода нет побочных эффектов, у компилятора также могут быть дополнительные оптимизации, которые он может выполнить. Это может быть намного проще для реализации этих оптимизаций. Гораздо проще даже концептуализировать оптимизацию для кода без побочных эффектов, а это означает, что ваш поставщик компиляторов может реализовать оптимизации, которые трудно-невозможно сделать в коде с побочными эффектами.
Параллелизм также значительно проще в реализации, для автоматической генерации и оптимизации, когда код не имеет побочных эффектов. Это потому, что все части можно безопасно оценить в любом порядке. Предоставление программистам возможности писать высококонкурентный код широко считается следующей большой проблемой, которую должна решить компьютерная наука, и одним из немногих оставшихся хеджей против закона Мура .
Побочные эффекты похожи на «утечки» в вашем коде, которые нужно будет обработать позже, либо вам, либо не подозревающему коллеге.
Функциональные языки избегают переменных состояния и изменяемых данных как способа сделать код менее зависимым от контекста и более модульным. Модульность гарантирует, что работа одного разработчика не повлияет / не подорвет работу другого.
Масштабирование скорости разработки с учетом размера команды - это «святой грааль» разработки программного обеспечения сегодня. При работе с другими программистами мало что важно, как модульность. Даже самые простые из логических побочных эффектов делают сотрудничество чрезвычайно трудным.
Ну, ИМХО, это довольно лицемерно. Никто не любит побочные эффекты, но они нужны всем.
Что опасно в побочных эффектах, так это то, что если вы вызываете функцию, то это, возможно, влияет не только на то, как ведет себя функция при следующем вызове, но и, возможно, на другие функции. Таким образом, побочные эффекты привносят непредсказуемое поведение и нетривиальные зависимости.
Парадигмы программирования, такие как ОО и функциональные, решают эту проблему. ОО уменьшает проблему, устанавливая разделение проблем. Это означает, что состояние приложения, состоящее из множества изменяемых данных, инкапсулируется в объекты, каждый из которых отвечает только за поддержание своего собственного состояния. Таким образом, уменьшается риск зависимости, а проблемы становятся гораздо более изолированными и их легче отслеживать.
Функциональное программирование использует гораздо более радикальный подход, когда состояние приложения просто неизменно с точки зрения программиста. Это хорошая идея, но делает язык бесполезным сам по себе. Почему? Потому что у ЛЮБОЙ операции ввода / вывода есть побочные эффекты. Как только вы читаете из любого входного потока, состояние вашего приложения, вероятно, изменится, потому что в следующий раз, когда вы вызовете ту же функцию, результат, скорее всего, будет другим. Возможно, вы читаете разные данные, или, возможно, операция не удалась. То же самое верно для вывода. Ровный вывод - это операция с побочными эффектами. В наши дни это часто не осознают, но представьте, что у вас есть только 20 КБ для вывода, и если вы выводите больше, ваше приложение падает, потому что у вас недостаточно места на диске или что-то еще.
Так что да, побочные эффекты неприятны и опасны с точки зрения программиста. Большинство ошибок происходит из-за того, что определенные части состояния приложения блокируются почти неясным способом, через необдуманные и часто ненужные побочные эффекты. С точки зрения пользователя, побочные эффекты - это смысл использования компьютера. Их не волнует, что происходит внутри или как это организовано. Они что-то делают и ожидают, что компьютер изменится соответственно.
Любой побочный эффект вводит дополнительные параметры ввода / вывода, которые необходимо учитывать при тестировании.
Это делает проверку кода намного более сложной, поскольку среда не может быть ограничена только проверяемым кодом, но должна включать в себя некоторую или всю окружающую среду (обновленная глобальная часть живет в этом коде, что, в свою очередь, зависит от этого). код, который, в свою очередь, зависит от жизни внутри полноценного сервера Java EE ....)
Стараясь избегать побочных эффектов, вы ограничиваете количество экстернализма, необходимого для запуска кода.
По моему опыту, хороший дизайн в объектно-ориентированном программировании требует использования функций, которые имеют побочные эффекты.
Например, возьмем простое настольное приложение пользовательского интерфейса. У меня может быть запущенная программа, имеющая в куче граф объектов, представляющий текущее состояние модели предметной области моей программы. Сообщения поступают к объектам в этом графе (например, через вызовы методов, вызываемые из контроллера уровня пользовательского интерфейса). Граф объектов (модель предметной области) в куче изменяется в ответ на сообщения. Наблюдатели модели информируются о любых изменениях, пользовательский интерфейс и, возможно, другие ресурсы модифицируются.
Отнюдь не зло, правильное расположение этих побочных эффектов, влияющих на изменение кучи и на экран, лежит в основе дизайна ОО (в данном случае это шаблон MVC).
Конечно, это не означает, что ваши методы должны иметь произвольные побочные эффекты. А функции, не имеющие побочных эффектов, имеют место для улучшения читаемости и иногда производительности вашего кода.
Как указывалось выше, функциональные языки не столько предотвращают побочные эффекты в коде, сколько предоставляют нам инструменты для управления тем, какие побочные эффекты могут возникнуть в данном фрагменте кода и когда.
Это имеет очень интересные последствия. Во-первых, и, что наиболее очевидно, есть множество вещей, которые вы можете сделать с помощью кода без побочных эффектов, которые уже были описаны. Но есть и другие вещи, которые мы можем сделать, даже работая с кодом, который имеет побочные эффекты:
В сложных основах кода сложные взаимодействия побочных эффектов - это самое трудное, о чем я нахожу рассуждать. Я могу говорить только лично, учитывая то, как работает мой мозг. Побочные эффекты и постоянные состояния, изменяющиеся входные данные и т. Д. Заставляют меня думать о том, «когда» и «где» происходят вещи, чтобы рассуждать о правильности, а не только о том, «что» происходит в каждой отдельной функции.
Я не могу просто сосредоточиться на "что". После тщательного тестирования функции, вызывающей побочные эффекты, я не могу сделать вывод о том, что она будет распространять атмосферу надежности по всему коду, использующему ее, поскольку вызывающие могут все еще использовать ее неправильно, вызывая ее в неправильное время, из неправильного потока, из неправильного заказ. Между тем, функцию, которая не вызывает побочных эффектов и просто возвращает новый вывод при условии ввода (не касаясь ввода), почти невозможно использовать таким способом.
Но я прагматичный тип, я думаю, или, по крайней мере, пытаюсь быть таковым, и я не думаю, что мы обязательно должны искоренять все побочные эффекты до минимума, чтобы рассуждать о правильности нашего кода (по крайней мере, Мне было бы очень трудно это сделать в таких языках, как 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). Но самое главное, я считаю, что вторую версию гораздо проще рассуждать с точки зрения правильности, а также изменений, не вызывая ошибок. Так что'
И, в конце концов, нам нужно, чтобы побочные эффекты возникали в какой-то момент, иначе у нас просто были бы функции, которые выводили данные, и некуда было бы идти. Часто нам нужно что-то записать в файл, отобразить что-то на экране, отправить данные через сокет, что-то в этом роде, и все эти вещи являются побочными эффектами. Но мы определенно можем уменьшить количество лишних побочных эффектов, которые продолжаются, а также уменьшить количество побочных эффектов, происходящих, когда потоки управления очень сложны, и я думаю, что было бы намного легче избежать ошибок, если бы мы это сделали.
Это не зло. На мой взгляд, необходимо различать два типа функций - с побочными эффектами и без. Функция без побочных эффектов: - всегда возвращает одно и то же с одинаковыми аргументами, поэтому, например, такая функция без каких-либо аргументов не имеет смысла. - Это также означает, что порядок, в котором некоторые такие функции называются, не играет роли - должен быть в состоянии работать и может быть отлажен только один (!), Без какого-либо другого кода. А теперь послушай, что делает JUnit. Функция с побочными эффектами: - имеет своего рода «утечки», что может быть выделено автоматически - это очень важно при отладке и поиске ошибок, которые обычно вызваны побочными эффектами. - Любая функция с побочными эффектами также имеет «часть» сама по себе без побочных эффектов, которую также можно разделить автоматически. Так злые эти побочные эффекты,