2016-01-25 2 views
3

Заметив, что мой код был по существу итерированием по списку и обновлением значения на Карте, я сначала создал тривиальный вспомогательный метод, который принял функцию для преобразования значения карты и возврата обновленной карты. По мере развития программы он получил несколько других функций Map-transform, поэтому было естественным превратить его в неявный класс значений, который добавляет методы к scala.collection.immutable.Map[A, B]. Эта версия работает нормально.Избегание понижений в общих классах

Однако нет ничего о методах, требующих конкретной реализации карты, и они, похоже, будут применяться к scala.collection.Map[A, B] или даже к MapLike. Поэтому я хотел бы, чтобы он был общим для типа карты, а также для ключей и значений. Здесь все идет грушевидно.

Моя текущая итерация выглядит следующим образом:

implicit class RichMap[A, B, MapType[A, B] <: collection.Map[A, B]](
    val self: MapType[A, B] 
) extends AnyVal { 
    def updatedWith(k: A, f: B => B): MapType[A, B] = 
    self updated (k, f(self(k))) 
} 

Этот код не компилируется, потому что self updated (k, f(self(k))) ISA scala.collection.Map[A, B], который не является MapType[A, B]. Другими словами, возвращаемый тип self.updated выглядит так, как если бы тип self был привязан к верхнему типу, а не к объявленному типу.

я могу «зафиксировать» код с опущенными:

def updatedWith(k: A, f: B => B): MapType[A, B] = 
    self.updated(k, f(self(k))).asInstanceOf[MapType[A, B]] 

Это не чувствует себя удовлетворительно, потому что это понижающее приведение код запах и указывает на неправильное использование системы типа. В этом конкретном случае казалось бы, что значение всегда будет типа cast-to и что вся программа компилируется и работает правильно с этим downcast, поддерживает это представление, но оно все еще пахнет.

Итак, есть ли лучший способ написать этот код, чтобы иметь правильные идентификаторы scalac без использования downcast, или это ограничение компилятора, а также сокращение?

[Отредактировано добавить следующее.]

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

var counts = Map.empty[Int, Int] withDefaultValue 0 
for (item <- items) { 
    // loads of other gnarly item-processing code 
    counts = counts updatedWith (count, 1 + _) 
} 

Есть три ответа на мой вопрос, на момент написания.

В любом случае, просто позвольте updatedWith вернуть scala.collection.Map[A, B]. По сути, мне нужна моя оригинальная версия, которая приняла и вернула immutable.Map[A, B], и делает тип менее конкретным. Другими словами, он все еще недостаточно общий и устанавливает политику, по которой используются вызывающие пользователи. Я могу, конечно, изменить тип в объявлении counts, но это также запах кода для работы с библиотекой, возвращающей неправильный тип, и все, что она действительно делает, - это перемещение вниз в код вызывающего. Так что мне совсем не нравится этот ответ.

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

В случае гипотетической неизменяемой Карты, которая разделяет большие деревья в аналогичном ключе на List, это копирование приведет к нарушению совместного использования и снижению производительности, и поэтому было бы предпочтительнее использовать существующие modified без выполнения копий. Тем не менее, неизменяемые карты Scala, похоже, не делают этого, и поэтому копирование (один раз) представляется прагматичным решением, которое вряд ли будет иметь какое-либо значение на практике.

+1

"гипотетический непреложным Map" Почему гипотетическая? HashMap имеет [гарантированное] (http://docs.scala-lang.org/overviews/collections/performance-characteristics.html) добавление постоянного времени (которое эффективно обновляется), что может быть достигнуто только путем совместного использования больших частей существующих данных. – Suma

ответ

5

Да! Используйте CanBuildFrom. Вот как библиотека коллекций Scala представляет самый близкий тип коллекции к тому, который вы хотите, используя доказательства CanBuildFrom. До тех пор, пока у вас есть скрытые доказательства CanBuildFrom[From, Elem, To], где From - тип коллекции, с которой вы начинаете, Elem - это тип, содержащийся в коллекции, и To - это конечный результат, который вы хотите. CanBuildFrom предоставит Builder, к которому вы можете добавить элементы, и когда вы закончите, вы можете позвонить Builder#result(), чтобы получить завершенную коллекцию соответствующего типа.

В этом случае:

From = MapType[A, B] 
Elem = (A, B) // The type actually contained in maps 
To = MapType[A, B] 

Реализация:

import scala.collection.generic.CanBuildFrom 

implicit class RichMap[A, B, MapType[A, B] <: collection.Map[A, B]](
    val self: MapType[A, B] 
) extends AnyVal { 
    def updatedWith(k: A, f: B => B)(implicit cbf: CanBuildFrom[MapType[A, B], (A, B), MapType[A, B]]): MapType[A, B] = { 
    val builder = cbf() 
    builder ++= self.updated(k, f(self(k))) 
    builder.result() 
    } 
} 

scala> val m = collection.concurrent.TrieMap(1 -> 2, 5 -> 3) 
m: scala.collection.concurrent.TrieMap[Int,Int] = TrieMap(1 -> 2, 5 -> 3) 

scala> m.updatedWith(1, _ + 10) 
res1: scala.collection.concurrent.TrieMap[Int,Int] = TrieMap(1 -> 12, 5 -> 3) 
+0

Не вызывает ли избыточная копия 'builder ++ = self.updated ...'? – pndc

+0

Похоже, что это было бы, но ручное внедрение того, что 'updated' делает по-настоящему, и использование' builder + = 'на самом деле кажется медленнее. –

+0

@ m-z Вероятно, это не так важно, но это так, как это делается в библиотеке повсюду, поэтому я бы предположил, что '+ =' должно быть быстрее. В любом случае код с '++ = s.updated (...)' выглядит определенно намного лучше. – Archeg

2

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

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

implicit class RichMap[A, B, MapType[x, y] <: Map[x, y]](val self: MapType[A, B]) extends AnyVal { 
    import scala.collection.generic.CanBuildFrom 
    def updatedWith[R >: B](k: A, f: B => R)(implicit bf: CanBuildFrom[MapType[A, B], (A, R), MapType[A, R]]): MapType[A, R] = { 
     val b = bf(self) 
     for ((key, value) <- self) { 
     if (key != k) { 
      b += (key -> value) 
     } else { 
      b += (key -> f(value)) 
     } 
     } 
     b.result() 
    } 
    } 

    import scala.collection.immutable.{TreeMap, HashMap} 

    val map1 = HashMap(1 -> "s", 2 -> "d").updatedWith(2, _.toUpperCase()) // map1 type is HashMap[Int, String] 
    val map2 = TreeMap(1 -> "s", 2 -> "d").updatedWith(2, _.toUpperCase()) // map2 type is TreeMap[Int, String] 
    val map3 = HashMap(1 -> "s", 2 -> "d").updatedWith(2, _.asInstanceOf[Any]) // map3 type is HashMap[Int, Any] 

Пожалуйста, обратите внимание, что CanBuildFrom картины гораздо более мощная и этот пример не использует все его мощность. Благодаря CanBuildFrom некоторые операции могут полностью изменить тип коллекции, как BitSet(1, 3, 5, 7) map {_.toString } Тип на самом деле SortedSet[String].

+0

Зачем связывать 'RichMap' с' mutable.Map'? –

+0

@ RégisJean-Gilles Нет причин. Я тестировал его с изменчивыми коллекциями, но пример является общим. Я удалил mutable упоминание – Archeg

Смежные вопросы