Неизменяемая карта Scala, когда переходить на изменяемую?

Мой нынешний вариант использования довольно тривиален, изменяемая или неизменяемая карта подойдет.

Имейте метод, который принимает неизменяемую карту, которая затем вызывает сторонний метод API, который также принимает неизменяемую карту.

def doFoo(foo: String = "default", params: Map[String, Any] = Map()) {

  val newMap = 
    if(someCondition) params + ("foo" -> foo) else params

  api.doSomething(newMap)
}

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

Тем не менее, это меня немного раздражает, копируя карту только для того, чтобы получить новую карту с несколькими прикрепленными к ней записями k-> v.

Я мог бы использовать изменяемые и params.put("bar", bar) и т. Д. Для записей, которые я хочу добавить, а затем params.toMap, чтобы преобразовать их в неизменяемые для вызова api, это вариант. но затем мне нужно импортировать и передавать изменяемые карты, что немного хлопотно по сравнению с использованием неизменяемой карты Scala по умолчанию.

Итак, каковы общие рекомендации, когда оправдано / рекомендуется использовать изменяемую карту вместо неизменяемой карты?

Спасибо

РЕДАКТИРОВАТЬ. Таким образом, похоже, что операция добавления к неизменяемой карте занимает почти постоянное время, подтверждая утверждение @ dhg и @ Nicolas о том, что полная копия не создается, что решает проблему для конкретного представленного случая.


person virtualeyes    schedule 13.04.2012    source источник
comment
См. Extreme Cleverness: Functional Data Structures in Scala.   -  person Daniel C. Sobral    schedule 13.04.2012


Ответы (4)


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

Такое поведение легче всего увидеть с помощью List. Если у меня val a = List(1,2,3), то этот список хранится в памяти. Однако, если я добавляю дополнительный элемент, например val b = 0 :: a, я действительно получаю обратно новый 4-элементный List, но Scala не копирует исходный список a. Вместо этого мы просто создали одну новую ссылку, назвали ее b и дали ей указатель на существующий список a.

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

person dhg    schedule 13.04.2012
comment
+1, круто, тогда это не проблема. Меня просто раздражала мысль о создании полной копии, когда нужно было добавить всего несколько k- ›v к исходной карте. - person virtualeyes; 13.04.2012
comment
Было бы интересно разъяснить, какие неизменяемые коллекции более устойчивы к дополнениям, а какие нет. Я ожидаю, что ListMaps будет не очень надежным. - person Andrew Norman; 17.02.2017

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

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

def buildVector( x: Double, y: Double, z: Double ): Vector[Double] = {
    val ary = Array.ofDim[Double]( 3 )
    ary( 0 ) = x
    ary( 1 ) = y
    ary( 2 ) = z
    ary.toVector
}

Теперь я думаю, что этот подход полезен / рекомендуется в двух случаях: (1) производительность, если создание и изменение неизменяемого объекта является узким местом всего вашего приложения; (2) Читаемость кода, потому что иногда проще изменить сложный объект на месте (вместо того, чтобы прибегать к линзам, молниям и т. Д.)

person paradigmatic    schedule 13.04.2012
comment
Да, альтернативой сохранению неизменяемого значения по умолчанию была передача изменяемой карты с последующим преобразованием в неизменяемую карту до выполнения вызова api, поэтому она не является функционально чистой по сравнению с вашим подходом «сделай все локально». Похоже, что для общего, не крайнего случая, неизменное значение по умолчанию подходит. Конечно, мне все еще неясно, как использовать изменяемую карту вместо неизменяемой. Надо было оставить дверь открытой, не приведя тривиальный случай, который заставил меня задать вопрос ... - person virtualeyes; 13.04.2012
comment
+1: это в основном шаблон компоновщика, используемый в неизменяемых коллекциях Microsoft BCL (nuget.org /packages/Microsoft.Bcl.Immutable) для .NET. - person Richard Cook; 19.05.2014

В дополнение к ответу dhg вы можете взглянуть на производительность коллекции scala. Если операция добавления / удаления не занимает линейное время, она должна делать что-то еще, кроме простого копирования всей структуры. (Обратите внимание, что обратное неверно: это не потому, что копирование всей структуры занимает линейное время)

person Nicolas    schedule 13.04.2012
comment
+1, отличная ссылка, спасибо. Согласно Performance & Characteristics, операция добавления неизменяемого HashMap занимает почти постоянное время, что в данном случае как раз то, что я хотел услышать ;-) - person virtualeyes; 13.04.2012

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

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

Я подозреваю, что через несколько лет мы оглянемся на три отдельных набора интерфейсов (изменяемые карты, неизменяемые карты и карты коллекций) и поймем, что в 99% случаев действительно нужны только 2 (изменяемые и коллекции) и что использование (к сожалению) неизменяемого интерфейса карты по умолчанию действительно добавляет много ненужных накладных расходов для «Масштабируемого языка».

person Andrew Norman    schedule 03.06.2016