В функциональном программировании почти все структуры данных являются неизменными, когда состояние должно измениться, создается новая структура. Значит ли это, что использование памяти будет намного больше? Я хорошо знаю парадигму объектно-ориентированного программирования, теперь я пытаюсь узнать о парадигме функционального программирования. Концепция того, что все неизменно, смущает меня. Казалось бы, программе, использующей неизменяемые структуры, потребуется гораздо больше памяти, чем программе с изменяемыми структурами. Я даже смотрю на это правильно?
functional-programming
Jbemmz
источник
источник
Ответы:
Единственный правильный ответ на это «иногда». Есть много приемов, которые функциональные языки могут использовать, чтобы не тратить память. Неизменность упрощает обмен данными между функциями и даже между структурами данных, поскольку компилятор может гарантировать, что данные не будут изменены. Функциональные языки, как правило, поощряют использование структур данных, которые могут эффективно использоваться как неизменяемые структуры (например, деревья вместо хеш-таблиц). Если вы добавляете лень в смесь, как это делают многие функциональные языки, это добавляет новые способы экономии памяти (это также добавляет новые способы тратить память, но я не буду вдаваться в подробности).
источник
Это зависит от структуры данных, точных внесенных вами изменений и, в некоторых случаях, от оптимизатора. В качестве одного примера давайте рассмотрим добавление в список:
Здесь потребность в дополнительной памяти постоянна, как и стоимость вызова во время выполнения
prepend
. Почему? Потому чтоprepend
просто создает новую клетку, у которой есть42
голова иlist1
хвост. Для этого не нужно копировать или иным образом перебиратьlist2
. То есть, за исключением памяти, необходимой для хранения42
,list2
повторно используется та же память, которая используетсяlist1
. Поскольку оба списка являются неизменными, это совместное использование совершенно безопасно.Аналогично, при работе со сбалансированными древовидными структурами большинству операций требуется только логарифмическое количество дополнительного пространства, потому что все, кроме одного пути дерева, может быть общим.
Для массивов ситуация немного другая. Вот почему во многих языках FP массивы не так часто используются. Однако, если вы делаете что-то подобное
arr2 = map(f, arr1)
иarr1
больше никогда не используете после этой строки, умный оптимизатор может фактически создавать код, который мутирует,arr1
вместо создания нового массива (не влияя на поведение программы). В этом случае производительность будет, как и на императивном языке, конечно.источник
Наивные реализации действительно разоблачили бы эту проблему - когда вы создаете новую структуру данных вместо обновления существующей на месте, у вас должны быть некоторые накладные расходы.
Разные языки по-разному справляются с этим, и большинство из них используют несколько хитростей.
Одна стратегия - сборка мусора . В тот момент, когда новая структура была создана, или вскоре после этого, ссылки на старую структуру выходят из области видимости, и сборщик мусора обнаружит ее мгновенно или достаточно быстро, в зависимости от алгоритма GC. Это означает, что, хотя накладные расходы все еще существуют, они носят временный характер и не будут расти линейно с объемом данных.
Еще один выбор различных типов структур данных. Там, где массивы являются структурой данных списка переходов в императивных языках (обычно заключенных в какой-то контейнер динамического перераспределения, такой как
std::vector
в C ++), функциональные языки часто предпочитают связанные списки. Со связанным списком операция prepend ('cons') может повторно использовать существующий список в качестве хвоста нового списка, поэтому все, что действительно выделяется - это новый заголовок списка. Подобные стратегии существуют для других типов структур данных - наборов, деревьев, вы называете это.А потом ленивая оценка, а-ля Хаскелл. Идея состоит в том, что создаваемые вами структуры данных создаются не полностью сразу; вместо этого они хранятся как «thunks» (вы можете думать о них как о рецептах для создания значения, когда это необходимо). Только когда значение требуется, thunk расширяется до фактического значения. Это означает, что выделение памяти может быть отложено до тех пор, пока не потребуется оценка, и в этот момент несколько блоков могут быть объединены в одном выделении памяти.
источник
Я только немного знаю о Clojure и о его неизменных структурах данных .
Графически мы можем представить что-то вроде этого:
источник
В дополнение к тому, что было сказано в других ответах, я хотел бы упомянуть язык программирования Clean, который поддерживает так называемые уникальные типы . Я не знаю этого языка, но полагаю, что уникальные типы поддерживают какое-то «деструктивное обновление».
Другими словами, хотя семантика обновления состояния заключается в том, что вы создаете новое значение из старого, применяя функцию, ограничение уникальности может позволить компилятору повторно использовать объекты данных для внутреннего использования, поскольку он знает, что на старое значение не будет ссылаться больше в программе после создания нового значения.
Для более подробной информации смотрите, например, Чистую домашнюю страницу и эту статью в Википедии.
источник