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

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

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

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

11 ответов

Решение

Читайте Функциональное программирование в двух словах.

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

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

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

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

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

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

На самом деле (из личного опыта) я обнаружил, что программирование на 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#, я понял, что мне стало проще кодировать функциональным образом, где, если вы хотите фильтровать, вы пишете "фильтр", а если вы хотите отобразить, вы пишете "карта" вместо реализации каждая из деталей.

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

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

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

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

Рассмотрим все сложные ошибки, которые вы долго отлаживали.

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

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

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

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

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

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

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

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

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

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

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

Недавно я написал пост на эту тему: " О важности чистоты".

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

Объект

      describe("counter", () => {
    it("should increment the count by one when 'increment' invoked without 
    argument", () => {
       const counter = new Counter(0)
       counter.increment()
       expect(counter.count).toBe(1)
    })
   it("should increment the count by n when 'increment' invoked with 
    argument", () => {
       const counter = new Counter(0)
       counter.increment(2)
       expect(counter.count).toBe(2)
    })
})

функциональный

       describe("incrementNumberBy(startingNumber, increment)", () => {

   it("should increment by 1 if n not supplied"){
      expect(incrementNumberBy(0)).toBe(1)
   }

   it("should increment by 1 if n = 1 supplied"){
      expect(countBy(0, 1)).toBe(1)
   }

 })

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

             const counter = new Counter(0)
       counter.increment()
       expect(counter.count).toBe(1)

Обе первые две строки вносят вклад в значение counter.count. В таком простом примере, как этот, 1 на 2 строки потенциально проблемного кода не имеет большого значения, но когда вы имеете дело с более сложным объектом, вы также можете добавить тонну сложности к своему тестированию.

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

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

Для функционального программирования: убедитесь, что функция A работает для заданных входов, вы убедитесь, что функция B работает с заданными входами, убедитесь, что C работает с заданными входами.

Для ООП: убедитесь, что метод объекта A работает с входным аргументом X после выполнения Y и Z для состояния объекта. Убедитесь, что метод объекта B работает с входным аргументом X после выполнения W и Y для состояния объекта.

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

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

Чтобы понять, почему это выгодно, вместо того, чтобы обращаться к литературе по функциональному программированию, мы можем обратиться к статье Дейкстры 1968 года «Перейти к утверждению, которое считается вредным»:

«Необузданное использование оператора go to приводит к тому, что становится ужасно трудно найти осмысленный набор координат для описания хода процесса».

Наблюдения Дейкстры, тем не менее, по-прежнему применимы к структурированным программам, которые избегают перехода к , потому что операторы типа while, if и еще много чего являются просто показухой для перехода к! Без использования go to мы все равно не сможем найти координаты, в которых можно описать ход процесса. Дейкстра не заметил, что у brided go to все те же проблемы.

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

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

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

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

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

Haskel и Prolog — хорошие примеры языков, которые могут быть реализованы как языки программирования без сохранения состояния. Но, к сожалению, они не так далеко. В настоящее время и Prolog , и Haskel имеют императивные реализации. Посмотрите некоторые SMT, они кажутся более близкими к кодированию без сохранения состояния.

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

Вот некоторые преимущества чистого безгражданства:

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

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

Другие вопросы по тегам