Подтвердить что ты не робот

Плюсы./Минусы. неизменности и мутируемости

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

Однако все это только то, о чем я читал. Я лично не сильно кодировал функциональные языки. И мне очень удивительно, что можно комфортно работать с неизменяемыми объектами. Теоретически это возможно. Но, с практической точки зрения, это очень удобный опыт. Или то, что новый вид рассуждений (для FP), который я должен развивать, чтобы мне не нужно изменчивости.

Я был бы признателен, как думать о написании программ, когда вы вынуждены использовать неизменяемые объекты.

  • Уже создав это Community Wiki... для всех, кто заинтересован в закрытии этого вопроса или обозначении этого как субъективного... и т.д. и т.д.... *
4b9b3361

Ответ 1

Многие функциональные языки не чисты (допускают мутацию и побочные эффекты).

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

Фокус в том, что это вообще-то:

  • Используйте экономно
  • Обратите внимание, когда вы
    • обратите внимание, как в f # вы должны объявить что-то изменяемым.

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

Это действительно 2, хотя все лучше. И это лучше всего сделать с помощью примера:

Возьмите свою базу кода и измените каждую переменную экземпляра на readonly [1] [2]. Измените только те, в которых вы нуждаетесь в них, чтобы быть изменяемыми для вашего кода, чтобы функционировать (если вы только устанавливаете их один раз за пределами конструктора, подумайте о том, чтобы попытаться сделать их аргументами для конструктора, а не изменяться с помощью чего-то вроде свойства.

Есть несколько базовых кодов, которые не будут хорошо работать с тяжелым кодом gui/widget и некоторыми библиотеками (особенно изменчивыми коллекциями), но я бы сказал, что наиболее разумный код позволит сделать более 50% всех полей экземпляров неизменяемые.

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

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


  • Я нахожу это неудачным, что С# не копировал java-концепцию неизменяемости с локальными переменными. Возможность решительно заявить, что что-то не меняется, помогает понять, стоит ли значение в стеке или в объекте/структуре.

  • Если у вас есть NDepend, вы можете найти их с помощью WARN IF Count > 0 IN SELECT FIELDS WHERE IsImmutable AND !IsInitOnly

Ответ 2

Неизверженность имеет несколько преимуществ, включая (но не ограничиваясь ими):

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

("foo" + "bar" + "baz").length()

  • В языках, где функции являются значениями первого класса, такие операции, как map, reduce, filter и т.д., являются основными операциями над коллекциями. Они могут быть объединены во многих отношениях и могут заменить большинство циклов в программе.

Есть, конечно, некоторые недостатки:

  • Циклические структуры данных, такие как графики, сложно построить. Если у вас есть два объекта, которые не могут быть изменены после инициализации, как вы можете заставить их указывать друг на друга?
  • Выделение много-много мелких объектов, а не изменение уже имеющихся у вас может иметь влияние на производительность. Обычно сложность распределителя или сборщика мусора зависит от количества объектов в куче.
  • Наивные реализации неизменяемых структур данных могут привести к крайне низкой производительности. Например, при объединении многих неизменяемых строк (например, в Java) O (n 2), когда лучшим алгоритмом является O (n). Можно писать эффективные неизменные структуры данных, это просто требует немного больше мысли.

Ответ 3

Но, с практической точки зрения, это очень удобный опыт.

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

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

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

Неизменяемые объекты должны быть небольшими

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

Рассмотрите свою базу данных своей компании, у вас могут быть таблицы с 20, 30, 50 свойствами. Если вам нужно изменить эти объекты во всем приложении, я бы определенно сопротивлялся желанию сделать эти неизменные.

С#/Java/С++ не являются хорошими функциональными языками. Вместо этого используйте Haskell, OCaml или F #

В моем собственном опыте неизменяемые объекты 1000 раз легче читать и писать на ML-подобных языках, чем на C-подобных языках. Извините, но как только у вас есть сопоставление типов и типы объединения, вы не можете их отдать:) Кроме того, некоторые структуры данных могут использовать оптимизацию хвостового вызова, функцию, которую вы просто не получаете в некоторых C- как Языки.

Но просто для удовольствия, здесь несбалансированное двоичное дерево в С#:

class Tree<T> where T : IComparable<T>
{
    public static readonly ITree Empty = new Nil();

    public interface ITree
    {
        ITree Insert(T value);
        bool Exists(T value);
        T Value { get; }
        ITree Left { get; }
        ITree Right { get; }
    }

    public sealed class Node : ITree
    {
        public Node(T value, ITree left, ITree right)
        {
            this.Value = value;
            this.Left = left;
            this.Right = right;
        }

        public ITree Insert(T value)
        {
            switch(value.CompareTo(this.Value))
            {
                case 0 : return this;
                case -1: return new Node(this.Value, this.Left.Insert(value), this.Right);
                case 1: return new Node(this.Value, this.Left, this.Right.Insert(value));
                default: throw new Exception("Invalid comparison");
            }
        }

        public bool Exists(T value)
        {
            switch (value.CompareTo(this.Value))
            {
                case 0: return true;
                case -1: return this.Left.Exists(value);
                case 1: return this.Right.Exists(value);
                default: throw new Exception("Invalid comparison");
            }
        }

        public T Value { get; private set; }
        public ITree Left { get; private set; }
        public ITree Right { get; private set; }
    }

    public sealed class Nil : ITree
    {
        public ITree Insert(T value)
        {
            return new Node(value, new Nil(), new Nil());
        }

        public bool Exists(T value) { return false; }

        public T Value { get { throw new Exception("Empty tree"); } }
        public ITree Left { get { throw new Exception("Empty tree"); } }
        public ITree Right { get { throw new Exception("Empty tree"); } }
    }
}

Класс Nil представляет пустое дерево. Я предпочитаю это представление над нулевым представлением, потому что нулевые проверки являются воплощением дьявола:)

Всякий раз, когда мы добавляем node, мы создаем новое дерево с вставкой node. Это более эффективно, чем кажется, потому что нам не нужно копировать все узлы в дереве; нам нужно только скопировать узлы "по пути вниз" и повторно использовать любые узлы, которые не изменились.

Скажем, у нас есть дерево, подобное этому:

       e
     /   \
    c     s
   / \   / \
  a   b f   y

Хорошо, теперь мы хотим вставить w в список. Мы начнем с корня e, перейдем к s, затем к y, а затем заменим y левого ребенка на w. Нам нужно создать копию узлов по пути вниз:

       e                     e[1]
     /   \                 /   \
    c     s      --->     c     s[1]
   / \   / \             / \    /\  
  a   b f   y           a   b  f  y[1]
                                  /
                                 w

Хорошо, теперь мы вставляем g:

       e                     e[1]                   e[2]
     /   \                 /   \                   /    \
    c     s      --->     c     s[1]      -->     c      s[2]
   / \   / \             / \    /\               / \     /   \
  a   b f   y           a   b  f  y[1]          a   b  f[1]   y[1]
                                  /                      \    /
                                 w                        g  w

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

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

Ответ 4

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

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

Марк Чу-Кэррол имеет приятную запись в блоге об этой теме.

Ответ 5

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

Императив:

for(int num = 0; num < 10; num++) {
    doStuff(num);
}

Функциональные:

def loop(num) :
    doStuff(num)
    if(num < 10) :
        loop(num + 1)

В этом случае вы копируете num на каждую итерацию и изменяете ее значение в процессе копирования.

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

Ответ 6

С неизменяемыми структурами данных, более вероятно, что операции над структурой данных будут разделять структуру, позволяя копиям быть дешевыми. (Clojure делает это)

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