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

Начнем с построения небольшой последовательности целых чисел для экспериментов:

val xs = Seq(4, 5, 2, -1, -3, 4)

Он будет использоваться в большинстве примеров в этом посте. Вы можете попробовать все фрагменты кода, представленные здесь, на листе Scala в IntelliJ IDEA или Scala IDE для Eclipse (или онлайн, используя Scastie).

И вот мы (в произвольном порядке):

перегородка

partition позволяет разбить коллекцию на две коллекции на основе заданного предиката. Так что вместо:

val even = xs.filter(_ % 2 == 0)
val odd = xs.filterNot(_ % 2 == 0)

ты можешь написать:

val (even, odd) = xs.partition(_ % 2 == 0)

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

minBy / maxBy

У вас может возникнуть соблазн отсортировать всю коллекцию, чтобы получить наименьшее (или наибольшее) значение. Это очень избыточный и неэффективный способ выполнения работы.

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

val minByAbs = xs.sortBy(Math.abs).head

Ты можешь написать:

val minByAbs = xs.minBy(Math.abs)

Фактически, IntelliJ IDEA предлагает вам такой рефакторинг автоматически. Если вы не сойдете с ума и не сделаете что-то вроде

val maxByAbs = xs.sortBy(Math.abs).reverse.head

в этом случае даже IntelliJ не сможет вам помочь ...

Просто имейте в виду, что и minBy, и maxBy выдадут UnsupportedOperationException в случае, если они будут вызваны для пустой коллекции (как и min и max). head и last, однако, выдают aNoSuchElementException в этих случаях (что, строго говоря, делает приведенные выше выражения неэквивалентными).

собирать / собирать

Иногда вы хотите как отфильтровать коллекцию, так и выполнить некоторое сопоставление ее значений. Например:

val doublesOfPositive = xs.filter(_ > 0).map(2 * _)

Это может быть красиво выражено с помощью collect (если вам нужно отфильтровать, а затем обработать) или collectFirst (если вам нужно обработать только первое значение, соответствующее предикату):

val doublesOfPositive = xs.collect {
  case x if x > 0 => 
    2 * x
}

collectFirst имеет тот же синтаксис, но возвращает Option (который равен None в случае, если не было значений, соответствующих предикату), и его можно безопасно использовать с пустыми коллекциями (как и find).

splitAt

splitAt - хороший способ разбить коллекцию на две смежные части по заданному индексу. Так что вместо

val leftHalf = xs.take(3)
val rightHalf = xs.drop(3)

ты можешь написать:

val (leftHalf, rightHalf) = xs.splitAt(3)

Это похоже на partition, разница в том, что вы разбиваете по индексу, а не по логическому предикату. Примечание: безопасно вызывать splitAt с индексом, превышающим размер вашей коллекции. В таком случае вы получите всю исходную коллекцию в leftHalf, а rightHalf будет пустым.

сгруппированы

grouped(n) разбивает коллекцию на коллекции по n элемента в каждой (последняя может содержать менее n элементов, в зависимости от размера исходной коллекции):

xs.grouped(3).toList // List(List(4, 5, 2), List(-1, -3, 4))

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

считать

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

val evenCount = xs.filter(_ % 2 == 0).size

Ты можешь написать:

val evenCount = xs.count(_ % 2 == 0)

(это также предлагается IntelliJ как автоматический рефакторинг).

существуют

Вам нужно проверить наличие какого-либо элемента, соответствующего заданному предикату? Вместо того

xs.find(_ % 42 == 0) != None

or

xs.count(_ % 42 == 0) > 0

Ты можешь написать:

xs.exists(_ % 42 == 0)

(или снова позвольте IntelliJ исправить это за вас).

последний

last делает именно это - возвращает последний элемент коллекции, поэтому нет необходимости в .reverse.head (или для доступа к последнему элементу по индексу).

И на десерт мой личный фаворит:

indexOfSlice / lastIndexOfSlice

В основном это касается производительности (в нотации Big O). Допустим, вам нужно найти данную подстроку в некоторой строке. Если вы опытный Java-разработчик, сразу скажите: используйте String.indexOf(substring). Конечно, будучи методом java.lang.String, он также доступен нам в Scala. Но подождите секунду! Что, если я (или какой-нибудь злонамеренный пользователь вашего общедоступного API) попытаюсь предоставить вам некоторые неудобные данные? Например:

val string = "a" * 2000000
val substring = "a" * 1000000 + "b"

string.indexOf(substring)

(на случай, если вам интересно, “foo” * x - хороший способ получить строку “foo”, повторяющуюся x раз в Scala).

Только не задерживайте дыхание, пытаясь запустить этот код! Вы потеряете его задолго до того, как код завершится. Это происходит потому, что реализация String.indexOf в Java использует наивный алгоритм сопоставления подстроки - для каждого вхождения первого символа поисковой подстроки ('a') он пытается сравнивать каждый последующий символ подстроки, пока не будет найдено несоответствие (или подстрока не будет полностью сопоставлена) . Этот конкретный ввод, который я дал, предназначен для выявления квадратичной (O(nm)) наихудшей временной сложности (n - длина основной строки, а m - длина подстроки поиска) этой реализации: выполнено один миллион успешных совпадений character'a' до того, как будет обнаружено первое несоответствие на 'b'. И это повторяется примерно миллион раз! Но чем здесь нам может помочь стандартная библиотека Scala? Давай попробуем:

val string = "a" * 2000000
val substring = "a" * 1000000 + "b"

string.indexOfSlice(substring)

Этот код делает то же самое, но завершается немедленно! Единственное изменение - замена indexOf на indexOfSlice (метод, который происходит от трейта SeqLike в стандартной библиотеке Scala). Но почему на этом входе он работает намного лучше? Если копнуть глубже, то под капотом этого метода можно найти реализацию алгоритма Кнута – Морриса – Пратта, который является действительно умным алгоритмом поиска подстроки. Он имеет линейную (O(n + m)) сложность даже в худшем случае (то есть - для любого возможного ввода). Если вы попытаетесь сравнить Java indexOf и Scala indesOfSlice на случайных входных данных, я уверен, что Java indexOf выиграет. Просто потому, что он специализируется на строках, а Scala indexOfSlice поддерживает последовательности универсального типа. Кроме того, реализация алгоритма наивного поиска имеет меньше накладных расходов. Однако indexOfSlice имеет преимущество, когда вам нужно защищаться от вводимых данных, которые вы не контролируете - например, когда ваши пользователи могут предоставить вам достаточно большие строки и инициировать поиск по ним. Кроме того, в Соревновательном программировании действительно имеет значение линейная или квадратичная сложность.

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

Вы думаете, я упустил несколько полезных методов коллекций Scala? Поделитесь своими любимыми в комментариях!