Избегание случайного удаления дубликатов при сопоставлении набора
Мне действительно нравятся функциональные концепции программирования, но я был укушен в двух отдельных случаях сейчас по той же самой игре, при сопоставлении с коллекцией, которая является Set
(т.е. автоматически удаляет дубликаты). Проблема заключается в том, что после преобразования элементов такого набора выходной контейнер также является множеством, и поэтому удаляет любые дубликаты преобразованного вывода.
Очень короткий сеанс REPL, чтобы проиллюстрировать проблему:
scala> case class Person(name: String, age: Int)
defined class Person
scala> val students = Set(Person("Alice", 18), Person("Bob", 18), Person("Charles", 19))
students: scala.collection.immutable.Set[Person] = Set(Person(Alice,18), Person(Bob,18), Person(Charles,19))
scala> val totalAge = (students map (_.age)).sum
totalAge: Int = 37
Я бы, конечно, ожидал, что общий возраст будет 18 + 18 + 19 = 55, а потому, что ученики были сохранены в Set
, так же как и их возраст после сопоставления, поэтому один из 18
исчез раньше возрасты были суммированы.
В реальном коде это часто более коварно и сложнее обнаружить, особенно если вы пишете код утилиты, который просто берет Traversable
и/или использует вывод методов, объявленных для возврата Traversable
(реализация который представляет собой набор). Мне кажется, что эти ситуации почти невозможно обнаружить надежно, пока/если они не проявятся как ошибка.
Итак, есть ли какие-либо рекомендации, которые уменьшат мою подверженность этой проблеме? Нехорошо думать о map
-переходе над общим Traversable как концептуально трансформируя каждый элемент на место, а не добавляя преобразованные элементы в свою очередь к некоторой новой коллекции? Должен ли я называть .toStream
на все перед сопоставлением, если я хочу сохранить эту ментальную модель?
Приветствуются любые советы/рекомендации.
Обновление. Большинство ответов до сих пор были сосредоточены на механике включения дубликатов в сумму. Меня больше интересует практика, связанная с написанием кода в общем случае - вы просверлили себя, чтобы всегда вызывать toList
в каждой коллекции до вызова map
? Вы старательно проверяете конкретные классы всех коллекций в своем приложении, прежде чем вызывать методы на них? Etc.
Фиксирование чего-то, что уже было идентифицировано как проблема, тривиально - трудная часть предотвращает проникновение этих ошибок в первую очередь.
Ответы
Ответ 1
Для этой цели вы можете использовать scalaz foldMap
, поскольку она работает на что угодно для которого существует Foldable
класс. Использование в вашем случае будет выглядеть так:
persons foldMap (_.age)
Подпись foldMap
выглядит следующим образом:
trait MA[M[_], A] {
val value: M[A]
def foldMap[B](f: A => B)(implicit f: Foldable[M], m: Monoid[B])
}
Итак, до тех пор, пока у вас есть некоторая коллекция CC[A]
, где CC
можно сложить (т.е. пройденную), функцию из A => B
, где B - моноид, вы можете скопировать результат.
Ответ 2
Как не перетаскивать дополнительные изображения в изображение:
(0 /: students) { case (sum, s) => sum + s.age }
Ответ 3
Вы можете breakOut тип коллекции
scala> import collection.breakOut
import collection.breakOut
scala> val ages = students.map(_.age)(breakOut): List[Int]
ages: List[Int] = List(18, 18, 19)
Затем вы можете суммировать как ожидалось
Основываясь на обновлении вопроса, наилучшей практикой предотвращения этих типов ошибок является хорошее покрытие unit test с репрезентативными данными вместе с разумным API в сочетании со знанием того, как компилятор scala поддерживает типы источников через map/для генераторов и т.д. Если вы возвращаете набор чего-то, вы должны сделать это очевидным, так как возврат Collection/Traversable скрывает соответствующую деталь реализации.
Ответ 4
Вы можете использовать методы toIterable
или toList
, чтобы сначала преобразовать набор в другую структуру данных. http://www.scala-lang.org/api/current/scala/collection/immutable/Set.html
(Обратите внимание, что toIterable
может возвращать любой Iterable, хотя ссылочная реализация не будет, согласно связанной документации. @Debilski сообщает мне в комментариях, что она тем не менее возвращает Set.)
Ответ 5
Если вы обнаружите, что несколько раз сталкиваетесь с одной и той же ошибкой, ваша первая проблема - не ошибка, а скорее повторение. map().sum
является достаточно распространенным вариантом использования (особенно в контексте анализа данных), чтобы заслужить его собственный метод на Traversable. Из моего личного, никогда не идущего-никуда-без-меняющегося класса сутенеров.
implicit def traversable2RichTraversable[A](t: Traversable[A]) = new {
///many many methods deleted
def sumOf[C: Numeric](g: A => C): C = t.view.toList.map(g).sum
///many many more methods deleted
}
(То, что .view
может не понадобиться, но не может повредить.)
Ответ 6
Неуклюжий, но возможно более быстрый способ его преобразования (по сравнению с явным toList
/toSeq
) будет использовать collection.breakOut
(дополнительную информацию) с
тип описи
(students map (_.age))(collection.breakOut) : Seq[Int]