По общему признанию, я склонен применять такие понятия в C ++ из-за языка и его природы, а также моего домена и даже того, как мы используем язык. Но, учитывая эти вещи, я думаю, что неизменяемые конструкции являются наименее интересным аспектом, когда речь заходит о том, чтобы пожинать массу преимуществ, связанных с функциональным программированием, таких как безопасность потоков, простота рассуждений о системе, поиск большего повторного использования для функций (и нахождение того, что мы можем объединять их в любом порядке без неприятных сюрпризов) и т. д.
Возьмите этот упрощенный пример C ++ (по общему признанию, не оптимизирован для простоты, чтобы не смущать себя перед любыми экспертами по обработке изображений):
// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
Image dst(new_w, new_h);
for (int y=0; y < new_h; ++y)
{
for (int x=0; x < new_w; ++x)
dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
}
return dst;
}
Хотя реализация этой функции изменяет локальное (и временное) состояние в виде двух переменных счетчика и временного локального изображения для вывода, у нее нет внешних побочных эффектов. Он вводит изображение и выводит новое. Мы можем многопоточность к содержанию наших сердец. Легко рассуждать, легко тщательно проверить. Это исключительная безопасность, поскольку, если что-то генерируется, новое изображение автоматически удаляется, и нам не нужно беспокоиться об откате внешних побочных эффектов (так сказать, внешние изображения не изменяются вне области действия функции).
Я вижу, что мало что можно получить и, возможно, многое можно потерять, сделав Image
неизменным в приведенном выше контексте в C ++, за исключением того, что потенциально можно сделать вышеуказанную функцию более громоздкой для реализации и, возможно, немного менее эффективной.
чистота
Поэтому чистые функции (без внешних побочных эффектов) очень интересны для меня, и я подчеркиваю важность их частого использования членами команды даже в C ++. Но неизменяемые конструкции, применяемые в основном в отсутствие контекста и нюансов, не так интересны для меня, поскольку, учитывая императивную природу языка, часто бывает полезно и полезно эффективно мутировать некоторые локальные временные объекты в процессе ( для разработчика и оборудования) реализация чистой функции.
Дешевое Копирование Здоровенных Структур
Второе наиболее полезное свойство, которое я нахожу, - это возможность дешево копировать действительно большие структуры данных, когда затраты на это, как это часто бывает, делают функции чистыми, учитывая их строгую природу ввода-вывода, будут нетривиальными. Это не будут небольшие структуры, которые могут поместиться в стеке. Они были бы большими, здоровенными структурами, как и все Scene
для видеоигры.
В этом случае накладные расходы на копирование могут помешать возможности эффективного параллелизма, потому что может быть сложно распараллелить физику и рендеринг эффективно, не блокируя и не ставя узкие места друг на друга, если физика мутирует сцену, которую визуализатор одновременно пытается нарисовать, и в то же время глубоко физически Копировать всю игровую сцену вокруг, чтобы вывести один кадр с применением физики, может быть неэффективно. Однако, если физическая система была «чистой» в том смысле, что она просто вводила сцену и выводила новую с применением физики, и такая чистота не достигалась за счет затрат на астрономическое копирование, она могла бы безопасно работать параллельно с рендер без одного ожидания на другом.
Таким образом, возможность дешево копировать действительно здоровенные данные о состоянии вашего приложения и выводить новые модифицированные версии с минимальными затратами на обработку и использование памяти может действительно открыть новые двери для чистоты и эффективного параллелизма, и там я нахожу много уроков, которые нужно изучить от того, как постоянные структуры данных реализованы. Но все, что мы создаем с использованием таких уроков, не обязательно должно быть полностью постоянным или предлагать неизменяемые интерфейсы (например, может использоваться копирование при записи или «построитель / переходный процесс»), чтобы достичь этой способности, чтобы быть очень дешевым копировать и изменять только фрагменты копии без удвоения использования памяти и доступа к памяти в нашем стремлении к параллелизму и чистоте в наших функциях / системах / конвейере.
неизменность
Наконец, есть неизменность, которую я считаю наименее интересной из этих трех, но она может привести в действие железным кулаком, когда определенные конструкции объектов не предназначены для использования в качестве локальных временных переменных для чистой функции, а вместо этого в более широком контексте, ценного своего рода «чистота на уровне объекта», поскольку во всех методах больше не вызывают внешних побочных эффектов (больше не изменяются переменные-члены вне непосредственной локальной области действия метода).
И хотя я считаю это наименее интересным из этих трех языков, таких как C ++, он, безусловно, может упростить тестирование, безопасность потоков и анализ нетривиальных объектов. Это может быть нагрузкой, чтобы работать с гарантией того, что объекту не может быть предоставлена какая-либо уникальная комбинация состояний вне его конструктора, например, и что мы можем свободно передавать его, даже посредством ссылки / указателя, не полагаясь на константность и читаемость. только итераторы и дескрипторы и тому подобное, гарантируя (ну, по крайней мере, столько, сколько мы можем в пределах языка), что его оригинальное содержимое не будет видоизменено.
Но я считаю это наименее интересным свойством, потому что большинство объектов, которые я нахожу столь же полезными, как временное использование в изменяемой форме для реализации чистой функции (или даже более широкой концепции, такой как «чистая система», которая может быть объектом или серией функции с конечным эффектом простого ввода чего-то и вывода чего-то нового, не затрагивая ничего другого), и я думаю, что неизменность, перенесенная в конечности в основном императивным языком, является довольно контрпродуктивной целью. Я бы применил это экономно для тех частей кодовой базы, где это действительно помогает больше всего.
В заключение:
[...] может показаться, что постоянные структуры данных сами по себе недостаточны для обработки сценариев, в которых один поток вносит изменения, видимые для других потоков. Для этого кажется, что мы должны использовать такие устройства, как атомы, ссылки, программную транзакционную память или даже классические блокировки и механизмы синхронизации.
Естественно, если ваш дизайн требует, чтобы изменения (в смысле пользовательского дизайна) были видны нескольким потокам одновременно, когда они происходят, мы возвращаемся к синхронизации или, по крайней мере, к чертежной доске, чтобы выработать некоторые сложные способы справиться с этим ( Я видел несколько очень сложных примеров, используемых экспертами, занимающимися подобного рода проблемами в функциональном программировании).
Но я обнаружил, что как только вы получаете такое копирование и возможность выводить частично модифицированные версии здоровенных структур, что очень дешево, как вы могли бы получить с постоянными структурами данных в качестве примера, это часто открывает множество дверей и возможностей, которые вы могли бы Раньше я не думал о том, чтобы распараллелить код, который может работать полностью независимо друг от друга, в строгом параллельном конвейере ввода-вывода. Даже если некоторые части алгоритма должны быть последовательными по своей природе, вы можете отложить эту обработку до одного потока, но обнаружите, что использование этих концепций открыло двери для простого и без беспокойства распараллеливания 90% тяжелой работы, например