Преимущества программирования без сохранения состояния?


132

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

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

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

Ответы:


168

Прочтите в двух словах о функциональном программировании .

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

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


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

@Ray: Я бы еще добавил распределенное программирование!
Антон Тихий

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

3
Кроме того, функциональное программирование на самом деле не связано с «безгражданством». Рекурсия - это уже неявное (локальное) состояние и это главное, что мы делаем в haskell. Это станет ясно, когда вы реализуете несколько нетривиальных алгоритмов в идиоматическом haskell (например, вычислительная геометрия) и получите удовольствие от их отладки.
hasufell 02

2
Не нравится приравнивать безгражданство к ПП. Многие программы FP заполнены состоянием, оно просто существует в замыкании, а не в объекте.
mikemaccana

46

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

Вы можете найти хорошее руководство с множеством примеров в статье Джона Хьюза « Почему функциональное программирование имеет значение» (PDF).

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


Разве миксины не будут предоставлять повторно используемый код аналогично ООП? Не защищать ООП, просто пытаюсь понять вещи самому.
mikemaccana

20

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

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

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

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

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Я понимаю, что сделать фильтр, а затем карту (то есть преобразование каждого элемента) в C # было бы довольно просто, но вы должны думать на более низком уровне. В частности, вам нужно будет написать сам цикл и иметь свой собственный явный оператор if и тому подобное. С тех пор, как я изучил F #, я понял, что мне легче кодировать функциональным способом, где, если вы хотите фильтровать, вы пишете «filter», а если вы хотите сопоставить, вы пишете «map» вместо реализации каждая из деталей.

Мне также нравится оператор |>, который, как мне кажется, отделяет F # от ocaml и, возможно, других функциональных языков. Это оператор конвейера, он позволяет «перенаправить» вывод одного выражения на вход другого выражения. Это заставляет код больше следовать тому, как я думаю. Как и в приведенном выше фрагменте кода, в нем говорится: «Возьмите последовательность факторов, отфильтруйте ее, а затем сопоставьте». Это очень высокий уровень мышления, которого нельзя достичь в императивных языках программирования, потому что вы так заняты написанием операторов цикла и if. Это единственное, чего мне больше всего не хватает, когда я перехожу на другой язык.

В общем, хотя я могу программировать как на C #, так и на F #, мне легче использовать F #, потому что вы можете думать на более высоком уровне. Я бы сказал, что, поскольку мелкие детали удалены из функционального программирования (по крайней мере, в F #), я более продуктивен.

Изменить : я видел в одном из комментариев, что вы просили пример «состояния» на функциональном языке программирования. F # можно написать императивно, поэтому вот прямой пример того, как можно иметь изменяемое состояние в F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i

1
Я в целом согласен с вашим сообщением, но |> не имеет ничего общего с функциональным программированием как таковым. На самом деле, a |> b (p1, p2)это просто синтаксический сахар для b (a, p1, p2). Соедините это с правоассоциативностью, и вы получите это.
Антон Тихий

2
Верно, я должен признать, что, вероятно, большая часть моего положительного опыта работы с F # больше связана с F #, чем с функциональным программированием. Но, тем не менее, между ними существует сильная корреляция, и хотя такие вещи, как вывод типов и |> не являются функциональным программированием как таковым, я бы определенно сказал, что они «идут в ногу с территорией». По крайней мере в целом.
Рэй Хидаят,

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

15

Подумайте обо всех сложных ошибках, на отладку которых вы потратили много времени.

Итак, сколько из этих ошибок произошло из-за «непреднамеренного взаимодействия» между двумя отдельными компонентами программы? (Почти все пронизывающие ошибки имеют такой вид: расы , подразумевающую запись общих данных, зависаний, ... Кроме того, он является общим , чтобы найти библиотеки , которые имеют неожиданное влияние на глобальном состоянии или чтения / записи реестра / окружающую среды и т.д.) Я постулирует, что по крайней мере 1 из 3 «серьезных ошибок» попадает в эту категорию.

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

Это большая победа от «неизменности» ИМО. В идеальном мире мы все разрабатывали бы потрясающие API-интерфейсы, и даже когда все было изменяемым, эффекты были бы локальными и хорошо документированными, а «неожиданные» взаимодействия были бы сведены к минимуму. В реальном мире существует множество API-интерфейсов, которые различными способами взаимодействуют с глобальным состоянием, и они являются источником самых пагубных ошибок. Стремление к безгражданству означает стремление избавиться от непреднамеренных / неявных / закулисных взаимодействий между компонентами.


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

8

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

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

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


Эта первая часть - действительно интересный момент, я никогда не думал об этом раньше. Спасибо!
Саша Чедыгов

Предположим, у вас есть sin(PI/3)в вашем коде, где PI - константа, компилятор может оценить эту функцию во время компиляции и встроить результат в сгенерированный код.
Artelius 02

6

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


Да, я определенно изучил это. В частности, очень интересна модель параллелизма в Erlang. Однако на данный момент меня действительно волнует не столько параллелизм, сколько производительность. Есть ли бонус к производительности от программирования без состояния?
Саша Чедыгов

2
@musicfreak, нет бонуса за производительность. Но как примечание, современные языки FP по-прежнему позволяют использовать состояние, если оно вам действительно нужно.
Неизвестный

В самом деле? Не могли бы вы привести пример состояния на функциональном языке, чтобы я мог увидеть, как это делается?
Саша Чедыгов

Посмотрите State Monad в Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
rampion

4
@ Неизвестно: я не согласен. Программирование без состояния снижает количество ошибок, вызванных непредвиденными / непреднамеренными взаимодействиями различных компонентов. Это также способствует лучшему дизайну (большему количеству повторного использования, разделению механизма и политики и тому подобному). Это не всегда подходит для поставленной задачи, но в некоторых случаях действительно блестит.
Artelius 02

6

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

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

Балансировщики нагрузки часто имеют возможность проводить «липкие сеансы», когда балансировщик нагрузки каким-то образом знает, на какой сервер отправлять запросы пользователей. Это не идеально, например, это означает, что каждый раз, когда вы перезапускаете свое веб-приложение, все подключенные пользователи теряют сеанс.

Лучший подход - хранить сеанс за веб-серверами в каком-то хранилище данных, в наши дни для этого доступно множество отличных продуктов nosql (redis, mongo, elasticsearch, memcached). Таким образом, веб-серверы не имеют состояния, но у вас все еще есть состояние на стороне сервера, и доступностью этого состояния можно управлять, выбрав правильную настройку хранилища данных. Эти хранилища данных обычно имеют большую избыточность, поэтому почти всегда должна быть возможность вносить изменения в ваше веб-приложение и даже хранилище данных, не затрагивая пользователей.


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