Коллекции Scala 2.13

  1. Под капотом: чистая земля
  2. Жизнь без CanBuildFrom
  3. Новый и достойный внимания
  4. groupMap
  5. Iterable - лучший тип коллекции
  6. LazyList предпочтительнее потока
  7. Операции вставки и удаления недоступны в общих коллекциях
  8. Резюме

Среда 13 июня 2018

Жюльен Ричард-Фой

Еще одна статья о стандартных коллекциях, правда? Действительно, за последние 18 месяцев была проделана большая работа в области коллекций, и мы опубликовали несколько статей в блогах и дали несколько выступлений, чтобы объяснить различные изменения или проблемы, с которыми мы столкнулись. Эта статья суммирует, что собирается измениться с точки зрения конечного пользователя .

Если вы внимательно следите за нашими предыдущими постами и беседами в блоге, вы можете не многому научиться из этой статьи. В противном случае, это прекрасная возможность наверстать упущенное за несколько минут!

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

Под капотом: чистая земля

Под капотом: чистая земля

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

Хорошая новость заключается в том, что новый дизайн более корректен в том смысле, что теперь вы можете реализовывать пользовательские нестрогие типы коллекций, не беспокоясь о повторной реализации множества операций. (Некоторые операции, тем не менее, все еще охотно оценивают элементы коллекции (например, groupBy) и будут четко задокументированы.) Еще одним преимуществом является то, что операции преобразования определяются вне коллекций (как в cvogt / Scala-расширения Project) теперь работают с нестрогими коллекциями (такими как View или Stream).

Говоря о нестрогих коллекциях, тип View был переработан, и представления должны вести себя более предсказуемо. Кроме того, Stream не рекомендуется в пользу LazyList (см. Последний раздел).

Жизнь без CanBuildFrom

Я думаю, что наиболее заметным изменением для конечных пользователей является то, что операции преобразования больше не используют CanBuildFrom. Я считаю, что это будет вполне заметно, несмотря на наши предыдущие попытки скрыть CanBuildFrom от документации API коллекций. Действительно, если вы посмотрите на старый 2.12 Список API подпись, показанная для операции сопоставления, не упоминает CanBuildFrom:

Однако, если вы используете эту операцию в своем коде, тогда ваша IDE показывает свою фактическую подпись:

Как видите, сигнатура типа, показанная в документации API, была «упрощена», чтобы сделать ее более доступной, но я полагаю, что это, вероятно, вводит пользователей в заблуждение. Особенно, когда вы смотрите на TreeMap [A, B] API :

Этот тип подписи не имеет смысла: тип результата не может быть TreeMap [B], поскольку TreeMap принимает два параметра типа (тип ключей и тип значений). Кроме того, функция f фактически принимает в качестве параметра пару ключ-значение, а не просто ключ (как неправильно указано типом A).

CanBuildFrom использовался по уважительным причинам, в частности, тип, показанный на приведенном выше снимке экрана, был вычислен в соответствии с типом исходной коллекции и типом элементов новой коллекции. Случай TreeMap убедителен: если вы преобразуете пары ключ-значение в другие пары ключ-значение, для которых тип ключей имеет неявный экземпляр Ordering, тогда map возвращает TreeMap, но если такого экземпляра Ordering не существует, тогда лучший тип коллекции, который может быть возвращен, это Карта. И если вы преобразуете пары ключ-значение во что-то, что даже не является парой, тогда лучшим типом коллекции, который может быть возвращен, является Iterable. Эти три случая были поддержаны единственной реализацией операции, и CanBuildFrom использовался для абстрагирования по различным возможным типам возвращаемых данных.

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

На практике это означает, что новый TreeMap имеет три перегрузки операции сопоставления:

На практике это означает, что новый TreeMap имеет три перегрузки операции сопоставления:

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

Новый и достойный внимания

Мы ввели несколько новых операций. В следующих разделах представлены некоторые из них.

groupMap

Обычный шаблон со старыми коллекциями 2.12 состоит в использовании groupBy, за которым следует mapValues, для преобразования групп. Например, вот как мы можем индексировать имена пользователей по возрасту:

case class User (name: String, age: Int) def namesByAge (users: Seq [User]): Map [Int, Seq [String]] = пользователи. groupBy (_. age). mapValues ​​(users => users. map (_. name))

В этом коде есть тонкость. Статический тип возврата - Map, но фактически возвращаемая реализация Map является ленивой и оценивает свои элементы каждый раз, когда ее обходят (т.е. функция users => users.map (_. Name) оценивается каждый раз, когда просматривается карта).

В новых коллекциях типом возвращаемого значения mapValues ​​является MapView, а не Map, чтобы четко указывать, что его содержимое оценивается каждый раз при его обходе.

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

def namesByAge (users: Seq [User]): Map [Int, Seq [String]] = users.groupMap (_. age) (_. name)

Возвращенная карта является строгой: она с готовностью оценивает свои элементы один раз. Кроме того, тот факт, что он реализован как одна операция, позволяет применять некоторые оптимизации, которые делают его в ~ 1,3 раза быстрее, чем версия, использующая mapValues.

Изменяемые коллекции имеют несколько новых операций для преобразования своих элементов на месте: вместо возврата новой коллекции (как это делают map и filter), они изменяют исходную коллекцию. К этим операциям добавляется InPlace. Например, чтобы удалить пользователей, чьи имена начинаются с буквы J из буфера, а затем увеличивать их возраст, теперь можно написать:

val users = ArrayBuffer (…) пользователи. filterInPlace (пользователь =>! пользователь. имя. начинается с ("J")). mapInPlace (пользователь => пользователь. копия (возраст = пользователь. возраст + 1))

Следствием очистки и упрощения структуры коллекций является то, что несколько типов или операций устарели в Scala 2.13 и будут удалены в 2.14.

Iterable - лучший тип коллекции

Нам показалось, что проводить различия между Traversable и Iterable не стоит, поэтому мы удалили Traversable (теперь это псевдоним Iterable [A]).

Iterable [A] теперь является типом коллекции на вершине иерархии. Его единственный абстрактный член - это def iterator: Iterator [A].

LazyList предпочтительнее потока

Поток устарел в пользу LazyList. Как следует из его названия, LazyList - это связанный список, элементы которого лениво оцениваются. Важное семантическое различие с Stream в том, что в LazyList голова и хвост ленивы, тогда как в Stream только хвост ленив.

Операции вставки и удаления недоступны в общих коллекциях

В старой платформе 2.12 тип scala.collection.Map имеет операции + и - для добавления и удаления записей. Семантика этих операций заключается в возвращении новой коллекции с добавленными или удаленными записями без изменения исходной коллекции.

Эти операции затем наследуются изменяемой ветвью коллекций. Но изменяемые типы коллекций также вводят свои собственные операции вставки и удаления, а именно + = и - =, которые изменяют исходную коллекцию на месте. Это означает, что тип scala.collection.mutable.Map имеет + и + =, а также - и - =.

Наличие всех этих операций может быть полезно в некоторых случаях, но также может привести к путанице. Если вы хотите использовать + или -, то вы, вероятно, хотели бы использовать неизменяемый тип коллекции в первую очередь ... Следовательно, +, - и обновленный были перемещены из scala.collection.Map в scala.collection.immutable.Map, и + и - были перемещены из scala.collection.Set в scala.collection.immutable.Set

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

Резюме

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

  • нестрогие коллекции (такие как представления) безопаснее в использовании и проще в реализации,
  • Сигнатуры типов операций преобразования (таких как map) проще (без неявного параметра CanBuildFrom),
  • добавлены новые классные операции,
  • иерархия типов более проста (не прослеживается),
  • изменяемые типы коллекций не наследуют неизменные операции вставки и удаления.