В прошлый раз мы разбирались с обработкой опциональных значений, выяснилось, что неправильно воспользовавшись элегантными средствами библиотеки Scala, можно продолжать стрелять себе по ногам.
В этот раз перейдём к коллекциям. Стандартная библиотека для коллекций в Scala настолько богата, что может предложить готовые методы даже для самых требовательных разработчиков. В каких случая применять какие методы обычно не описывается и всё познаётся на опыте. Обычно, в начале все узнают filter
и map
, на этом дело может ограничиться, ведь с определённой фантазией можно реализовать множество алгоритмов только на этих функциях. Однако, эти способы могут быть неоптимальны или рождать невозможные для предметной области результаты, которые, однако же, придётся обработать.
Ниже мы обсудим, какие функции стандартной библиотеки часто используются неправильно и что можно улучшить.
foreach и for(val i = 0; i < n; i++)
При переходе на Scala у многих возникает ломка из-за отсутствия for(;;)
. Кто-то, особенно кто писал на C, считает, что без такого цикла вообще нельзя писать программы, более продвинутые пытаются писать аналоги на while
с выносом начальных условий и шага цикла во внешний код, например:
var i = 0 // Опять var!
while(i < n) {
// Сделаем что-то нужное, зависящее от i
i = i+1
}
На самом деле самый точный аналог for(val i = 0; i< n; i++)
— это for(i <- 0 until n)
,
так что тут всё было на поверхности, хотя с ходу это не всегда видно. Чтобы писать правильно нужно знать работу класса Range и видеть примеры его использования на блогах, типа Code Commit.
Iterator
Частенько при работе со старым кодом на Java прилетают классы, которые и проитерировать-то нормально нельзя. В качестве примера возьмём java.sql.ResultSet
(Enumeration
тоже катит). Он не является Iterable, то есть для него нет прозрачного преобразования в JavaConversions
, и с ним приходится работать исключительно в императивном виде или копировать его в содержимое в промежуточную mutable-коллекцию, как вариант, строить immutable через builder. Страшно.
Для этого случая в стандартной библиотеке Scala есть класс Iterator, который позволяет сделать из ResultSet
съедобный Iterator
, который хотя бы Traversable
:
val resultSetIterator = Iterator.continually(resultSet).takeWhile(_.next())
Простые замены и бинарная логика
Начнём с простых правил замены, когда цепочку вызовов можно заменить более простыми или даже одним простым методом. Самые простые методы, которые могут вам помочь — exists
, forall
и find
. Частенько я наблюдал:
option.map(myValueIsGoodEnough).getOrElse(false)
, вместо
option.exists(goodEnoughValue) // даже можно `option exists goodEnoughValue`
потом
iterable.filter(goodEnough).headOption
, вместо
iterable.find(goodEnough)
даже было такое (правда, прям у нас в коде!)
iterable.foldLeft(true)(_ && goodEnough)
вместо
iterable.forall(goodEnough)
Стоит ли говорить, что более простые варианты не только более просты для восприятия, но и реализованы более эффективно, за счёт того, что map
, filter
и foldLeft
будут проверять всю коллекцию, какой бы длинной она не была, тогда как exists
, find
и forall
закончатся сразу, как только найдут подходящий (или неподходящий) элемент и не будут порождать дополнительную промежуточную коллекцию.
На курсе по Scala на Coursera.com проходили также несколько полезных функций: span
, dropWhile
и takeWhile
. Использование их тоже может быть не очевидно на практике, ведь filter
успешно работает для этих случаев тоже, правда менее эффективно, ведь он не знает, что с какого-то момента элементы проверять уже не нужно: они все уже точно подходят или не подходят. Например, я сам до недавнего времени убирал начало и конец последовательности курсов валют, не входящие в интервал, через filter
, а не через dropWhile
и takeWhile
.
Агрегаты
Методы fold
, foldLeft
и foldRight
очень мощны и с помощью них можно решить совершенно разные задачи
Остановимся, например, на foldLeft
, с его помощью можно получить сумму элементов коллекции:
iterable.foldLeft(0) { (accum, elem) => accum + elem }
Канонический вариант написания для такого кода следующий:
iterable.foldLeft(0) { _ + _ }
Мартин Одерский для этого показывал интересный вариант на Scala Days:
(0 /: iterable)(_ + _)
говорит, как будто домино падают слева направо :) Подробнее плюсы описаны тут, но я всё же скептически отношусь к символьным операторам, если они не общеизвестны для предметной области.
В любом случае, в данной ситуации не стоит писать этот код, ведь существует специальный метод для коллекции с элементами из typeclass Numeric
, можно написать проще:
iterable.sum
Также доступны функции min
и max
(особые умельцы любят сортировать коллекцию и брать head/tail, хорошо у нас такого не видел).
Методы для свёртки лучше приберечь для применения один за другим шагов высокоуровневого алгоритма, например в chain-of-responsibility или сделать reduce в вашем map-reduce.
Иногда при обработке графиков и вообще информации распределённой во времени приходится обрабатывать элементы попарно (текущий и предыдущий), в этом случае может очень захотеться, мне хотелось, использовать for(;;)
и играться с индексами,
for(i <- 1 until seq.size) {
result ++= combine(seq(i-1), seq(i))
}
в Scala же можно сшить последовательность с самой собой, сдвинутой на один элемент и дальше уже применять функцию, которая обрабатывает оба элемента:
seq.zip(seq.drop(1)).map(combine) // так для последовательности
list.zip(list.tail).map(combine) // , а так для листа
, только не забудьте проверить, что лист не пустой!
Тот же код правильнее написать, как
seq.grouped(2).map(calculateDelta)
А теперь хорошие новости, на JavaOne получилось зарепортить многие из этих упрощений ребятам из IntelliJ, и уже к Scala Days они успели реализовать их и ещё много других
У ребят теперь специальный человек занимается Scala-инспекциями, так что если у вас появились интересные мысли по упрощению — создавайте тикеты, всё будет!
Пишите хороший код и будьте счастливы.
Автор: vuspenskiy