Как вы можете сделать что-нибудь полезное без изменяемого состояния? - PullRequest
237 голосов
/ 20 июня 2009

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

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

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

РЕДАКТИРОВАТЬ: куча ответов до сих пор, кажется, пытаются убедить меня в преимуществах неизменных ценностей. Я получил эту часть. Это имеет смысл. Чего я не понимаю, так это как вы можете отслеживать значения, которые должны изменяться, и постоянно изменяться без изменяемых переменных.

Ответы [ 17 ]

146 голосов
/ 20 июня 2009

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

Если вам интересно, вот серия статей, в которых описывается программирование игр с Erlang.

Вероятно, вам не понравится этот ответ, но вы не получите функциональную программу , пока не воспользуетесь ею. Я могу опубликовать примеры кода и сказать: «Здесь, не так ли? 1011 * см. », но если вы не понимаете синтаксис и основные принципы, тогда ваши глаза просто глазурят. С вашей точки зрения, похоже, что я делаю то же самое, что и императивный язык, но просто устанавливаю все виды границ, чтобы целенаправленно усложнять программирование. Моя точка зрения, вы просто испытываете парадокс Blub .

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

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

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

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Метод Seq.iter будет перечислять коллекцию и вызывать анонимную функцию для каждого элемента. Очень удобно:)

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

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

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

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

Почти каждое пользовательское приложение, которое я может думать, включает в себя государство как ядро концепция.

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

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Приведенный выше код создает два неизменяемых списка, складывает их вместе, чтобы создать новый список, и добавляет результаты. Никакое изменяемое состояние не используется нигде в приложении. Это выглядит немного громоздко, но это только потому, что C # - многословный язык. Вот эквивалентная программа на F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

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

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

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

73 голосов
/ 20 июня 2009

Краткий ответ: вы не можете.

Так что же тогда за постоянство?

Если вы хорошо разбираетесь в императивном языке, значит, вы знаете, что «глобалы плохие». Зачем? Потому что они вводят (или имеют потенциал для внедрения) некоторые очень трудно распутываемые зависимости в вашем коде. И зависимости не хороши; Вы хотите, чтобы ваш код был модульным . Части программы не влияют на другие части как можно меньше. И FP приводит вас к святому Граалю модульности: никаких побочных эффектов вообще . У вас просто есть f (x) = y. Положи х, получи у. Без изменений х или чего-либо еще. FP заставляет вас перестать думать о состоянии и начать думать с точки зрения ценностей. Все ваши функции просто получают значения и производят новые значения.

Это имеет несколько преимуществ.

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

Во-вторых, это делает программу тривиально распараллеливаемой (эффективное распараллеливание - другой вопрос).

В-третьих, возможны некоторые преимущества в производительности. Скажем, у вас есть функция:

double x = 2 * x

Теперь вы вводите значение 3, а вы получаете значение 6. Каждый раз. Но вы можете сделать это в обязательном порядке, верно? Ага. Но проблема в том, что в императиве вы можете сделать даже больше . Я могу сделать:

int y = 2;
int double(x){ return x * y; }

но я тоже мог бы сделать

int y = 2;
int double(x){ return x * (y++); }

Императивный компилятор не знает, будут ли у меня побочные эффекты или нет, что усложняет оптимизацию (т. Е. Double 2 не обязательно должно быть 4 каждый раз). Функциональный знает, что я не буду - следовательно, он может оптимизировать каждый раз, когда он видит "двойной 2".

Теперь, хотя создание новых значений каждый раз кажется невероятно расточительным для сложных типов значений с точки зрения компьютерной памяти, это не должно быть так. Потому что, если у вас есть f (x) = y, а значения x и y "в основном одинаковы" (например, деревья, которые отличаются только несколькими листами), то x и y могут совместно использовать части памяти - потому что ни одно из них не будет мутировать .

Так что, если эта неизменяемая вещь настолько велика, почему я ответил, что без изменяемого состояния вы ничего не сможете сделать. Ну, без изменчивости, вся ваша программа была бы гигантской функцией f (x) = y. И то же самое относится ко всем частям вашей программы: только к функциям и функциям в «чистом» смысле. Как я уже сказал, это означает, что f (x) = y каждый раз. Так, например readFile ("myFile.txt") должен будет возвращать одно и то же строковое значение каждый раз. Не слишком полезно.

Таким образом, каждый FP предоставляет некоторое средство изменения состояния. «Чистые» функциональные языки (например, Haskell) делают это, используя несколько пугающие концепции, такие как монады, в то время как «нечистые» (например, ML) позволяют это напрямую.

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

25 голосов
/ 20 июня 2009

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

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

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

Приложение: (Относительно вашего редактирования того, как отслеживать значения, которые необходимо изменить)
Конечно, они будут храниться в неизменной структуре данных ...

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

Очевидно, что в практических решениях вы бы использовали более вменяемый подход, но это показывает, что в худшем случае, если ничего не сработает, вы могли бы «симулировать» изменяемое состояние с помощью такой карты, которую вы переносите через дерево вызовов.

15 голосов
/ 20 июня 2009

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

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

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

Рассмотрим этот код:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Довольно болотный стандартный императивный код. Ничего интересного не делает, но это нормально для иллюстрации. Я думаю, вы согласитесь, что здесь участвует государство. Значение переменной x меняется со временем. Теперь давайте немного изменим обозначение, придумав новый синтаксис:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Поставьте скобки, чтобы было яснее, что это значит:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

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

Вы обнаружите, что этот шаблон может моделировать любое состояние, даже IO.

11 голосов
/ 02 июля 2009

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

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

становится таким функциональным кодом (Схемоподобный синтаксис):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

или этот Haskellish код

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

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

Хороший учебник с множеством примеров можно найти в статье Джона Хьюза Почему важно функциональное программирование .

10 голосов
/ 20 июня 2009

Это просто разные способы сделать одно и то же.

Рассмотрим простой пример, такой как добавление чисел 3, 5 и 10. Представьте себе, что вам нужно сделать это, сначала изменив значение 3, добавив 5 к нему, затем добавив 10 к этому «3», а затем выведя текущий значение «3» (18). Это кажется явно нелепым, но это, по сути, то, как часто делается основанное на состоянии императивное программирование. Действительно, у вас может быть много разных «3», которые имеют значение 3, но разные. Все это кажется странным, потому что мы так глубоко укоренились в весьма разумной идее, что числа неизменны.

Теперь подумайте о добавлении 3, 5 и 10, когда вы принимаете значения неизменяемыми. Вы добавляете 3 и 5 для получения другого значения, 8, затем добавляете 10 к этому значению, чтобы получить еще одно значение, 18.

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

7 голосов
/ 28 июля 2016

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

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

Первый императивный путь (в псевдокоде)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

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

predicate ? if-true-expression : if-false-expression

Вы можете связать троичное выражение, поместив новое троичное выражение вместо ложного выражения

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Итак, имея в виду, вот функциональная версия.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

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

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

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

Моя рекомендация - сделать Маленького Планировщика (обратите внимание, что я говорю «делай», а не «читай»), а затем делай все упражнения в SICP. Когда вы закончите, у вас будет другой мозг, чем когда вы начали.

6 голосов
/ 20 июня 2009

На самом деле довольно легко иметь что-то, что выглядит как изменяемое состояние, даже в языках без изменяемого состояния.

Рассмотрим функцию с типом s -> (a, s). В переводе с синтаксиса Haskell это означает, что функция принимает один параметр типа "s" и возвращает пару значений типов "a" и "s". Если s является типом нашего состояния, эта функция принимает одно состояние и возвращает новое состояние и, возможно, значение (вы всегда можете вернуть «единицу измерения» или «1008 *», что эквивалентно «void» в C / C ++, как тип "a"). Если вы объединяете несколько вызовов функций с такими типами (получение состояния, возвращаемого из одной функции и передача его следующей), вы получаете «изменяемое» состояние (фактически вы находитесь в каждой функции, создавая новое состояние и отказываясь от старого. ).

Возможно, будет легче понять, если вы представите изменяемое состояние как «пространство», в котором выполняется ваша программа, а затем подумаете о временном измерении. В момент времени t1 «пространство» находится в определенном состоянии (например, в некоторой ячейке памяти есть значение 5). В более поздний момент времени t2 он находится в другом состоянии (например, ячейка памяти теперь имеет значение 10). Каждый из этих временных «кусочков» является состоянием, и он неизменен (вы не можете вернуться во времени, чтобы изменить их). Итак, с этой точки зрения, вы перешли от полного пространства-времени со стрелкой времени (ваше изменяемое состояние) к набору кусочков пространства-времени (несколько неизменяемых состояний), и ваша программа просто обрабатывает каждый кусочек как значение и вычисляет каждый из них как функция, примененная к предыдущему.

ОК, может быть, это было не так легко понять: -)

Может показаться неэффективным явное представление всего состояния программы как значения, которое должно быть создано только для того, чтобы отбрасываться в следующий момент (сразу после создания нового). Для некоторых алгоритмов это может быть естественным, но когда это не так, есть другой прием. Вместо реального состояния вы можете использовать ложное состояние, которое является не чем иным, как маркером (давайте назовем тип этого ложного состояния State#). Это поддельное состояние существует с точки зрения языка и передается как любое другое значение, но компилятор полностью его пропускает при генерации машинного кода. Он служит только для обозначения последовательности выполнения.

В качестве примера предположим, что компилятор предоставляет нам следующие функции:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

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

Если вы вызываете readRef и затем передаете ему ложное состояние, возвращаемое writeRef (возможно, с другими вызовами несвязанных функций в середине; эти значения состояния создают «цепочку» вызовов функций), он возвращает значение написано. Вы можете снова вызвать writeRef с тем же указателем / дескриптором, и он запишет в ту же область памяти & mdash; но, поскольку концептуально он возвращает новое (поддельное) состояние, состояние (поддельное) все еще остается неизменным (новое «создано»). Компилятор будет вызывать функции в том порядке, в котором он должен был бы вызывать их, если бы существовала переменная реального состояния, которую нужно было вычислить, но единственное состояние, которое существует, - это полное (изменяемое) состояние реального оборудования.

(Те, кто знает Haskell, заметят, что я многое упростил и пропустил несколько важных деталей. Для тех, кто хочет увидеть больше деталей, взгляните на Control.Monad.State из mtl, а также ST s и IO (он же ST RealWorld) монады.)

Вы можете задаться вопросом, зачем делать это окольным путем (вместо того, чтобы просто иметь изменяемое состояние в языке). Реальное преимущество в том, что у вас есть reified состояние вашей программы. То, что раньше было неявным (состояние вашей программы было глобальным, допуская такие вещи, как действие на расстоянии ), теперь явное. Функции, которые не получают и не возвращают состояние, не могут изменять его или влиять на него; они "чисты". Более того, вы можете иметь отдельные потоки состояний, и с небольшим количеством магии типов они могут использоваться для встраивания императивных вычислений в чистые, не делая их нечистыми (монада ST в Haskell - это та, которая обычно используется для этот трюк: State#, о котором я упоминал выше, на самом деле является State# s GHC, используемым при реализации монад ST и IO.

6 голосов
/ 20 июня 2009

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

4 голосов
/ 20 июня 2009

В дополнение к отличным ответам, которые дают другие, подумайте о классах Integer и String в Java. Экземпляры этих классов являются неизменяемыми, но это не делает классы бесполезными только потому, что их экземпляры нельзя изменить. Неизменность дает вам некоторую безопасность. Вы знаете, что если вы используете экземпляр String или Integer в качестве ключа для Map, ключ не может быть изменен. Сравните это с классом Date в Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

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

...