что может пойти не так в контексте функционального программирования, если мой объект изменчив?

9

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

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

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

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

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

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

rahulaga_dev
источник
1
@Ruben Я бы сказал, что большинство функциональных языков допускают изменчивые переменные, но используют их по-разному, например изменяемые переменные имеют другой тип
jk.
1
Я думаю, что вы могли смешать неизменное и изменчивое в первом абзаце?
JK.
1
@jk. Он, конечно, сделал. Отредактировано, чтобы исправить это.
Дэвид Арно
6
@Ruben Функциональное программирование - это парадигма. Как таковой, он не требует функционального языка программирования. И некоторые языки fp, такие как F #, имеют эту функцию .
Кристоф
1
@Ruben нет, конкретно я думал о Mvars в haskell hackage.haskell.org/package/base-4.9.1.0/docs/… конечно, разные языки имеют разные решения или IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html, хотя, конечно, вы бы использовали оба из монад
jk.

Ответы:

7

Я думаю, что важность лучше всего продемонстрировать, сравнивая с ОО-подходом

например, скажем, у нас есть объект

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

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

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

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

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

Вы ожидаете order.Status == "Куплено"?

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

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Если заказ был изменен с помощью функции покупки, купленный заказ2 потерпит неудачу.

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

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

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

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

Ewan
источник
Спасибо !! очень полезно. Таким образом, новая реализация покупки будет выглядеть Order Purchase() { return new Order(Status = "Purchased") } так, что статус будет доступен только для чтения. ? Опять же, почему эта практика более актуальна в контексте парадигмы программирования функций? Упомянутые вами преимущества можно увидеть и в ОО-программировании, верно?
rahulaga_dev
в OO можно ожидать, что object.Purchase () изменит объект. Вы можете сделать его неизменным, но тогда почему бы не перейти к полной функциональной парадигме
Эван
Я думаю, что проблема заключается в том, чтобы визуализировать, потому что я чистый разработчик C #, который ориентирован на объект по своей природе. Так что то, что вы говорите на языке, охватывающем функциональное программирование, не потребует функции «Покупка ()», возвращающей купленный заказ, который должен быть присоединен к какому-либо классу или объекту, верно?
rahulaga_dev
3
вы можете написать функционал c #, изменить свой объект на структуру, сделать его неизменным и написать Func <Order, Order> Purchase
Ewan
12

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

Ключевой вопрос, который нужно спросить, что это за преимущество неизменности? Ответ в том, что он избегает сложности. Скажем, у нас есть две переменные, xи y. Оба начинаются со значения 1. yхотя удваивается каждые 13 секунд. Какова будет ценность каждого из них через 20 дней? xбудет 1. Это легко. Хотя это потребует усилий, так yкак это намного сложнее. Какое время суток через 20 дней? Нужно ли учитывать летнее время? Сложность yпротив xгораздо больше.

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

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

Дэвид Арно
источник
4
«Один из самых простых способов уменьшить сложность состоит в том, чтобы сделать объекты неизменяемыми по умолчанию и сделать их изменяемыми только при необходимости»: очень хорошее и краткое резюме.
Джорджио
2
@DavidArno Сложность, которую вы описываете, затрудняет анализ кода. Вы также затронули это, когда говорите: «Код трудно писать; трудно читать; трудно отлаживать; ...». Мне нравятся неизменяемые объекты, потому что они делают код намного проще, не только для меня, но и для наблюдателей, которые следят, не зная всего проекта.
разборка-номер-5
1
@RahulAgarwal, « Но почему эта проблема становится более заметной в контексте функционального программирования ». Это не так. Я думаю, может быть, меня смущает то, что вы спрашиваете, поскольку проблема в FP гораздо менее заметна, поскольку FP способствует неизменности, что позволяет избежать этой проблемы.
Дэвид Арно
1
@djechlin, « Как ваш 13-й пример может быть легче проанализировать с помощью неизменяемого кода? » Он не может: yдолжен мутировать; это требование. Иногда мы должны иметь сложный код для удовлетворения сложных требований. Суть, которую я пытался подчеркнуть, заключается в том, что ненужных сложностей следует избегать. Мутирующие значения по своей природе более сложны, чем фиксированные, поэтому, чтобы избежать ненужной сложности, изменяйте значения только тогда, когда это необходимо.
Дэвид Арно
3
Изменчивость создает кризис идентичности. Ваша переменная больше не имеет единой идентичности. Вместо этого его личность теперь зависит от времени. Таким образом, символически, вместо одного x, у нас теперь есть семейство x_t. Любой код, использующий эту переменную, теперь также должен беспокоиться о времени, вызывая дополнительную сложность, упомянутую в ответе.
Алекс Вонг
8

что может пойти не так в контексте функционального программирования

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

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

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

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

Док Браун
источник
4

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

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

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

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
Карл Билефельдт
источник
3

Преимущество использования неизменяемых объектов состоит в том, что если кто-то получает ссылку на объект, у которого будет определенное свойство, когда получатель проверяет его, и ему нужно дать некоторому другому коду ссылку на объект с тем же свойством, то можно просто передать вдоль ссылки на объект без учета того, кто еще мог получить ссылку или что они могли бы сделать с объектом [поскольку никто ничего не может сделать с объектом], или когда получатель мог бы исследовать объект [так как все его свойства будут одинаковыми независимо от того, когда они проверяются].

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

Я думаю, что для программирования в целом (а не только для функционального программирования) наиболее полезно думать о неизменных объектах как о трех категориях:

  1. Объекты, которые не могут ничего изменить, даже со ссылкой. Такие объекты и ссылки на них ведут себя как значения и могут свободно использоваться.

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

  3. Объекты, которые будут изменены. Эти объекты лучше всего рассматривать как контейнеры , а ссылки на них - как идентификаторы .

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

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

Supercat
источник
спасибо за впечатляющий ответ !! Я думаю, вероятно, источник моей путаницы в том, что я из c # background и изучаю «написание кода функционального стиля в c #», который везде говорит: избегайте изменяемых объектов - но я думаю, что языки, которые поддерживают парадигму функционального программирования, продвигают (или применяют - не уверен если принудительно использовать правильно) неизменяемость.
rahulaga_dev
@RahulAgarwal: можно иметь ссылки на объект, инкапсулирующие значение , значение которого не зависит от существования других ссылок на тот же объект, иметь идентичность, которая связывает их с другими ссылками на тот же объект, или ни на одно. Если состояние реального слова изменяется, то либо значение, либо идентичность объекта, связанного с этим состоянием, могут быть постоянными, но не обоими - нужно будет измениться. 50 000 долларов - это то, что должно делать.
суперкат
1

Как уже упоминалось, проблема с изменяемым состоянием в основном является подклассом более крупной проблемы побочных эффектов , когда возвращаемый тип функции не точно описывает, что функция действительно делает, потому что в этом случае она также выполняет мутацию состояния. Эта проблема была решена в некоторых новых исследовательских языках, таких как F * ( http://www.fstar-lang.org/tutorial/ ). Этот язык создает систему эффектов, аналогичную системе типов, где функция не только статически объявляет свой тип, но и свои эффекты. Таким образом, вызывающие функции осознают, что при вызове функции может происходить мутация состояния, и этот эффект распространяется на ее вызывающих.

Аарон М. Эшбах
источник