Профессионалы. / Недостатки. из Неизменности по сравнению с Переменчивостью

Другое голосование за Dina. Пока Вы используете его в его оптимальном размере (9 ПБ), это выглядит большим.

alt text

22
задан user855 7 December 2009 в 22:57
поделиться

6 ответов

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

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

Хитрость в том, что это что-то общее:

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

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

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

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

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

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

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


  1. Мне очень жаль, что C # не скопировал концепцию неизменности Java с локальными переменными. Возможность категорически утверждать, что что-то не меняется, помогает понять намерение, находится ли значение в стеке или в объекте / структуре.

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

4
ответ дан 29 November 2019 в 04:26
поделиться

Просто укажите его в свойстве shared.loader или common.loader из /conf/catalina.properties .

  • Операции с неизменяемыми объектами возвращают новые неизменяемые объекты, а операции, вызывающие побочные эффекты для изменяемых объектов, обычно возвращают void . Это означает, что несколько операций можно связать вместе. Например,
  • ("foo" + "bar" + "baz"). Length ()

    • В языках, где функции являются значениями первого класса, такие операции, как map , , уменьшают ], фильтр и т. Д. Являются основными операциями над коллекциями. Их можно комбинировать разными способами, и они могут заменить большинство циклов в программе.

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

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

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

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

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

    Я был бы признателен, если бы об этом подумал писать программы, когда вас заставляют to use immutable objects.

    Immutable objects should be small

    For the most part, I find immutable data structures easiest to work with when the objects have fewer than 3 or 4 intrinsic properties. For example, each node in a red-black tree has 4 properties: a color, a value, a left child, and right-child. A stack has two properties, a value and a pointer to the next stack node.

    Consider your company's database, you might have tables with 20, 30, 50 properties. If you need to modify those objects throughout your application, then I'd definitely resist the urge to make those immutable.

    C# / Java / C++ aren't good functional languages. Use Haskell, OCaml, or F# instead

    In my own experience, immutable objects are 1000x easier to read and write in ML-like languages than C-like languages. I'm sorry, but once you have pattern matching and union types, you can't give them up :) Additionally, some data structures can take advantage of tail-call optimization, a feature you just don't get in some C-like languages.

    But just for fun, here's an unbalanced binary tree in 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"); } }
        }
    }
    

    The Nil class represents an empty tree. I prefer this representation over the null representation because null checks are the devil incarnate :)

    Whenever we add a node, we create a brand new tree with the node inserted. This is more efficient than it sounds, because we don't need to copy all the nodes in the tree; we only need to copy nodes "on the way down" and reuse any nodes which haven't changed.

    Let's say we have a tree like this:

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

    Alright, now we want to insert w into the list. We're going to start at the root e, move to s, then to y, then replace y's left child with a w. We need to create copy of the nodes on the way down:

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

    Alright, now we insert a 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
    

    We reuse all of the old nodes in the tree, so there's no reason to rebuild the entire tree from scratch. This tree has the same computational complexity as its mutable counterpart.

    Its also pretty easy to write immutable versions of red-black trees, AVL trees, tree-based heaps, and many other data structures.

    8
    ответ дан 29 November 2019 в 04:26
    поделиться

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

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

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

    5
    ответ дан 29 November 2019 в 04:26
    поделиться

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

    Императив:

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

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

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

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

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

    1
    ответ дан 29 November 2019 в 04:26
    поделиться

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

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

    2
    ответ дан 29 November 2019 в 04:26
    поделиться
    Другие вопросы по тегам:

    Похожие вопросы: