Представляю вашему вниманию перевод статьи Павла Фатина Scala Collections Tips and Tricks. Павел работает в JetBrains и занимается разработкой Scala плагина для IntelliJ IDEA. Далее, повествование идет от лица автора. В этой статье вы найдете упрощения и оптимизации, характерные для повседневного использования API Scala коллекций. Некоторые советы основаны на тонкостях реализации библиотеки коллекций, однако большинство рецептов — это разумные преобразования, которые на практике часто упускаются из виду. Этот список вдохновлен моими попытками разработать практичные инспекции для Scala коллекций, для Scala плагина IntelliJ. Сейчас мы внедряем эти инспекции, так что, используя Scala плагин в IDEA, вы автоматически выигрываете от статического анализа кода. Тем не менее, эти рецепты ценны сами по себе. Они могут помочь вам углубить понимание стандартной библиотеки коллекций Scala и сделать ваш код быстрее и выразительнее. Обновление:
Если вы испытываете тягу к приключениям,
вы можете узнать, как помочь в развитии IntelliJ плагина для Scala и попробовать свои силы в реализации, подобрав подходящую инспекцию. Содержание: Все примеры кода доступны в репозитории на GitHub. Чтобы сделать примеры кода понятней, я использовал следующие обозначения: Помните, вопреки тому, что рецепты изолированы и самодостаточны, их можно скомпоновать для последующего постепенного превращения в более продвинутые выражения: Так, мы можем полагаться на "заменяющую модель применения рецептов" (аналогично SICP), и использовать ее для упрощения сложных выражений. "Побочный эффект" (Side effect) это основное понятие, которое стоит рассмотреть перед тем, как мы перечислим основные преобразования. По сути, побочный эффект — любое действие, которое наблюдается за пределами функции или выражения помимо возврата значения, например: О функциях или выражениях, содержащих любое из вышеперечисленных действий, говорят, что они имеют побочные эффекты, в противном случае их называют «чистыми». Почему побочные эффекты так важны? Потому что с ними порядок исполнения имеет значение. Например, два «чистых» выражения, (связанных с соответствующими значениями): Так как они не содержат побочных эффектов (т.е. эффектов, наблюдаемых вне выражений), мы можем вычислить эти выражения в произвольном порядке — сначала x, а затем y или сначала y, а затем x — это не повлияет на корректность полученных результатов (мы можем даже закешировать результирующие значения, если того захотим). Теперь рассмотрим следующую модификацию: А это уже другая история — мы не можем изменить порядок выполнения, потому что в нашем терминале будет напечатано "barfoo" вместо "foobar" (и это явно не то, чего хотелось). Так, присутствие побочных эффектов сокращает число возможных преобразований (как упрощений, так и оптимизаций), которые мы можем применить к коду. Схожие рассуждения применимы и к родственным коллекциям, выражениям. Представим, что где-то за пределами области видимости у нас есть некий builder: В принципе, вызов seq.filter(p).headOption упрощается до seq.find(p), впрочем, наличие побочного эффекта не дает нам это сделать: Хотя эти выражения и эквивалентны с позиции конечного значения, они не эквивалентны касательно побочных эффектов. Первое выражение добавит все элементы, а последнее отбросит все элементы, как только найдет первое совпадающее с предикатом значение. Поэтому такое упрощение сделать нельзя. Что можно сделать для того, чтобы автоматическое упрощение стало возможным? Ответ — это золотое правило, которого следует придерживаться по отношению ко всем побочным эффектам в нашем коде (включая тот, где коллекций нет в принципе): Поэтому нам нужно либо избавиться от builderа (вместе с его API, в котором есть побочные эффекты), либо отделить вызов builderа от чистого выражения. Предположим, что этот builder является неким сторонним объектом, изжить который мы не можем, так что нам остается лишь изолировать вызов: Теперь мы можем благополучно выполнить преобразование: Чисто и красиво! Изоляция побочных эффектов сделала возможным автоматическое преобразование. Дополнительная выгода еще и в том, что из-за присутствия четкого разделения, человеку легче понять получившийся код. Наименее очевидным и при этом наиболее важным преимуществом изоляции побочных эффектов будет улучшение надежности нашего кода вне зависимости от других возможных оптимизаций. Касательно примера: первоначальное выражение может порождать различные побочные эффекты, зависящие от текущей реализации Seq. Для Vector, например, оно добавит все элементы, для Stream оно пропустит все элементы после первого удачного сопоставления с предикатом (потому что стримы «ленивы» — элементы вычисляются только тогда, когда это необходимо). Отделение побочных эффектов позволяет нам избежать этих неопределенностей. Хотя советы в этом разделе и относятся к наследникам Seq, некоторые преобразования допустимы и для других типов коллекций (и не коллекций), например: Set, Option, Map и даже Iterator (потому что все они предоставляют похожие интерфейсы с монадическими методами). Некоторые неизменяемые (immutable) классы коллекций имеют синглтон-реализацию метода empty. Однако, далеко не все фабричные методы проверяют длину созданных коллекций. Так что, обозначив пустоту на этапе компиляции, вы можете сохранить либо место в куче (путем переиспользования экземпляра), либо такты процессора (которые могли бы быть потрачены на проверки размерности во время выполнения).
Также применимо к: Set, Option, Map, Iterator. Хотя size и length по существу синонимы, в Scala 2.11 вызовы Array.size по-прежнему выполняются через неявное преобразование (implicit conversion), таким образом создавая промежуточные объекты-обертки для каждого вызова метода. Если вы, конечно, не включите эскейп анализ для JVM, временные объекты станут обузой для сборщика мусора и ухудшат производительность кода (особенно внутри циклов). Простые свойства содержат меньше визуального шума, чем составные выражения.
Также применимо к: Set, Option, Map, Iterator. С одной стороны, простое свойство воспринимается гораздо легче, чем составное выражение. С другой стороны, наследникам LinearSeq (таким как List) может потребоваться O(n) времени на вычисление длины списка (вместо O(1) для IndexedSeq), таким образом мы можем ускорить наш код, избегая вычисления длины, когда нам, вобщем-то, это значение не очень-то и нужно.
Имейте также в виду, что вызов .length для бесконечных стримов может никогда не закончиться, поэтому всегда проверяйте стрим на пустоту явно.
Также применимо к: Set, Map. Поскольку расчет размера коллекции может быть достаточно «дорогим» вычислением для некоторых типов коллекций, мы можем сократить время сравнения с O(length) до O(length min n) для наследников LinearSeq (которые могут быть спрятаны под Seq-подобными значениями). Кроме того, такой подход незаменим, когда имеем дело с бесконечными стримами. Разумеется, такой трюк будет совсем излишним.
Также применимо к: Set, Option, Map, Iterator. Проверка на равенство всегда будет выдавать false для различных экземпляров массивов.
Также применимо к: Iterator. Проверки на равенство могут быть использованы для сравнения коллекций и различных категорий (например List и Set).
Прошу вас дважды подумать о смысле данной проверки (касательно примера выше — как рассматривать дубликаты в последовательности). Проверка равенства — это способ, которым следует сравнивать коллекции одной и той же категории. В теории это может улучшить производительность из-за наличия возможных низлежащих проверок экземпляра (eq, обычно намного быстрее). У нас уже есть встроенный метод, который делает тоже самое. Оба выражения принимают во внимание порядок элементов. И мы опять-таки сможем выиграть пользу от повышения производительности. Для некоторых классов коллекций обновленный подход может быть немного быстрее (ознакомьтесь с кодом List.apply, например). К тому же, доступ к свойству намного проще (как синтаксически, так и семантически), чем вызов метода с аргументом. Последнее выражение будет понятней и позволит избежать ненужного вычисления длины коллекции (а для линейных последовательностей это может занять немало времени). Более того, некоторые классы коллекций могут извлекать последний элемент более эффективно в сравнении с доступом по индексу. Семантически второе выражение эквивалентно, однако более выразительно Упрощенное выражение более лаконично. Последнее выражение короче (и потенциально быстрее). Из-за особенностей работы вариантности, методы indexOf и lastIndexOf принимают аргументы типа Any. На практике это может приводить к труднонаходимым багам, которые невозможно обнаружить на этапе компиляции. Вот где будут к месту вспомогательные инспекции вашей IDE. У нас есть встроенный метод, который возвращает диапазон из всех индексов последовательности. Во-первых, последнее выражение короче. Кроме того, мы можем ожидать некоторый прирост производительности, из-за того, что мы избегаем скрытого вычисления размера коллекции (что в случае линейных последовательностей может обойтись недешево).
Дополнительное преимущество последнего выражения в том, что оно хорошо работает с потенциально бесконечными коллекциями (например Stream). Поскольку экземпляр IndexedSeq[T] также является Function1[Int, T], вы можете использовать его как таковой. Второе выражение семантически эквивалентно, при этом более выразительно. Когда эти выражения применяются к Set, производительность может разительно отличаться, потому что поиск у множеств стремится к O(1) (из-за внутреннего индексирования, не использующегося при вызове exists).
Также применимо к: Set, Option, Iterator. Так же как методы indexOf и lastIndexOf, contains принимает аргументы типа Any, что может привести к труднонаходимым багам, которые не обнаруживаются на этапе компиляции. Будьте с ними осторожны. И снова последнее выражение чище и, вероятно, быстрее (особенно для множеств).
Также применимо к: Set, Option, Iterator. Очевидно, когда нам нужно знать, находится ли соответствующий условию элемент в коллекции, подсчет количества удовлетворяющих элементов будет излишним. Упрощенное выражение выглядит чище и работает быстрее. Вызов filter создает промежуточную коллекцию, которая занимает место в куче и нагружает GC. К тому же, предшествующие выражения находят все вхождения, в то время как требуется найти только первое (что может замедлить код в зависимости от возможного содержимого коллекции). Потенциальный выигрыш в производительности менее значим для ленивых коллекций (таких как Stream и, в особенности, Iterator). Поиск определенно лучше фильтрации, однако и это далеко не предел (по крайней мере, с точки зрения ясности).
Также применимо к: Set, Option, Map, Iterator. Последнее выражение синтактически проще (при том, что семантически они эквивалентны).
Также применимо к: Set, Option, Map, Iterator. Вызов filter создает промежуточную (и не очень-то нужную) коллекцию, которая будет занимать место в куче и нагружать GC.
Также применимо к: Set, Option, Map, Iterator. Конечно, если seq не является ленивой коллекцией (как, например, Stream), фильтрация найдет все вхождения (и создаст временную коллекцию) при том, что требовался только первый элемент.
Также применимо к: Set, Option, Map, Iterator. Для этого у нас есть свой метод, более ясный и выразительный. И для этого тоже есть метод. Таким образом, мы можем избежать создания промежуточной коллекции и исключить дополнительные преобразования (чтобы сберечь место в куче и циклы процессора). Последний подход более выразителен. Кроме того, из-за того что не создается дополнительная коллекция, работать он будет быстрее. Объяснение совпадает с предыдущим советом. Преимущества этого подхода — ясность и выразительность. Причины те же, что и в предыдущем случае. Обоснование такое же, как и в предыдущем случае.
Также применимо к: Set, Iterator. Все как и в предыдущем случае.
Также применимо к: Set, Iterator. Цель упрощения — ясность и выразительность. При всей ясности и выразительности, последнее выражение может работать быстрее (оно останавливает последующую обработку элементов, как только найдет первый подходящий элемент), что может работать для бесконечных последовательностей. Это «классическая» в функциональном программировании реализация отображения (map) через свертку. Бесспорно, она поучительна, но нужды ее использовать нет. Для этого у нас есть встроенный и выразительный метод (который еще и быстрее, так как в своей реализации использует простой цикл while).
Также применимо к: Set, Option, Iterator. Причины те же, что и в предыдущем случае.
Также применимо к: Set, Option, Iterator. И опять-таки встроенный метод быстрее и чище.
Также применимо к: Set, Option, Iterator. Вот несколько обособленных советов, посвященных сопоставлению с образцом в Scala и частичным функциям. Обновленное выражение дает сходный результат и выглядит при этом проще. Описанные выше преобразования можно применить к любым функциям, а не только к аргументам функции map. Этот совет относится не только к коллекциям. Однако, в виду вездесущести функций высшего порядка в API стандартной библиотеки коллекций Scala, он будет весьма кстати. Обновленное выражение дает аналогичный результат и выглядит намного проще.
Также применимо к: Set, Option, Map, Iterator. Учитывая, что все case-операторы создают коллекции, можно упростить выражение, заменив match на вызов collect. Так мы создаем коллекцию всего один раз, опустив при этом явные ветки case для дефолтных случаев.
Лично я обычно использую этот трюк с Option, а не с последовательностями как таковыми.
Также применимо к: Set, Option, Iterator. Для такого случая у нас есть особый метод, который работает быстрее для неленивых коллекций. Так мы можем избежать создания промежуточной коллекции (после первого вызова filter), чем облегчим участь сборщика мусора.
Мы так же можем использовать обобщенный подход, который полагается на представления (смотрите ниже), получив: seq.view.filter(p1).filter(p2).force. Как и в предыдущем случае, мы сразу создаем конечную коллекцию без создания промежуточной. Мы так же можем применить обобщенный подход, который полагается на view (смотрите ниже), получив: seq.view.map(f).map(g).force. Сортировка — процедура затратная. Поэтому нет нужды сортировать элементы, которые на следующем шаге могут быть отфильтрованы. Первое выражение создает промежуточную (перевернутую) коллекцию перед преобразованием элементов, что иногда бывает достаточно разумно (например для List). В других случаях, что будет более эффективно, можно сразу выполнить требуемые преобразования, не создавая промежуточную коллекцию. К тому же последнее выражение проще и может быть более эффективным. Нет нужды создавать временное множество (во всяком случае явно), чтобы найти отдельные элементы. Для линейных последовательностей, ничего кроме ясно выраженных мыслей и намерений мы не получим. Однако, в случае с индексированными последовательностями мы можем ожидать потенциальный прирост производительности.
Также применимо к: Set, Map, Iterator. Для линейных последовательностей (как для List, так и для Stream), упрощенные выражения будут выполняться быстрее из-за того, что результаты вычисляются за один проход.
Также применимо к: Set, Map. А так мы можем пройти последовательность и проверить предикат не два, а всего один раз. Опять-таки, преимуществом будет вычисление в один проход Последнее выражение более выразительно и потенциально более эффективно (как для индексированных, так и для линейных последовательностей). Нет необходимости делать это вручную: у нас уже есть встроенный метод.
Также применимо к: Set, Option, Iterator. Опять-таки незачем писать велосипед. Улучшится не только выразительность, дополнительная коллекция создаваться тоже не будет.
Также применимо к: Set, Option, Iterator. Когда вам нужны именно побочные эффекты, оправданий вызову map нет. Такой вызов вводит в заблуждение, при том еще и менее эффективен.
Также применимо к: Set, Option, Map, Iterator. Незачем создавать дополнительные коллекции, когда требуется всего-навсего один элемент. Этот рецепт разбит на три части (в зависимости от конечного результата преобразования). 1) Преобразование сокращает коллекцию до единственного значения. Вместо reduce может быть любой метод, который сокращает коллекцию до единственного значения, например: reduceLeft, reduceRight, fold, foldLeft, foldRight, sum, product, min, max, head, headOption, last, lastOption, indexOf, lastIndexOf, find, contains, exists, count, length, mkString, и т.д. Точный порядок преобразований не столь важен — важно то, что мы создаем одну, а то и несколько промежуточных коллекций не очень-то и нужных, при этом они будут занимать место в куче и нагружать GC. Это происходит потому, что по умолчанию все преобразователи коллекций (map, flatMap, filter, ++, и т.д.) являются «строгими» (за исключениемStream) и, как результат, порождают новую коллекцию со всеми ее элементами. Здесь на помощь приходят представления (view) — о которых вы можете думать, как о своего рода итераторах, позволяющих повторную итерацию: Чтобы перейти от коллекции к ее представлению, используйте метод view. 2) Преобразование, порождающее коллекцию того же типа. Представления можно использовать и тогда, когда конечный результат преобразования по-прежнему остается коллекцией — метод force построит коллекцию первоначального типа (при этом все промежуточные коллекции созданы не будут): Если фильтрация — единственное промежуточное преобразование, то, как вариант, вы можете рассмотреть метод withFilter: Первоначально этот метод предназначался для использования внутри "for comprehensions". Он работает так же, как и представление — создает временный объект, который ограничивает область последующих преобразований коллекции (так, что он реорганизует возможные побочные эффекты). Однако, нет нужды явно преобразовывать коллекцию к (или наоборот) от временного представления (вызвав veiw и force) Хоть основанный на представлениях подход и будет более универсальным в этом конкретном случае, withFilter из-за лаконичности может быть более предпочтительным. 3) Преобразование порождает коллекцию другого типа. В этот раз вместо обобщенного вызова force мы используем подходящий метод-конвертер, поэтому результатом будет коллекция другого типа. Также существует альтернативный способ совладать с «преобразованием + конверсией». И случай этот полагается на breakOut: Функционально выражение эквивалентно использованию представления, однако: Так что, скорее всего, лучше заменить breakOut на более гибкое и выразительнее представление. Представления наиболее целесообразны при относительно большом размере коллекций. Scala предлагает «синтаксический сахар», известный как «операторы присваивания» (“assignment operators”) — он автоматически приводит операторы типа x <op>= y к виду x = x <op> y, где: <op> некий символьный оператор (например: +, -, и т.д). Обратите внимание, что если <op> заканчивается на :, то он считается право-ассоциативным (т.е. вызывается для правого выражения, вместо левого). Для списков и стримов также существует особый синтаксис: Упрощенные выражения лаконичны.
Также применимо к Set, Map, Iterator (учитывая специфику операторов). Для этого существуют встроенные методы, которые и чище, и быстрее. А если вам нужно преобразовать или отфильтровать значения во время преобразования, рассмотрите использование представлений или схожих техник, описанных выше.
Также применимо к: Set, Option, Iterator. Из-за того, что Seq(...) создает строгую коллекцию (а именно, Vector), мы можем захотеть использовать toSeq для преобразования нестрогой сущности (как Stream, Iterator или view) к строгой коллекции. Однако TraversableOnce.toSeq на самом деле возвращает Stream, являющийся ленивой коллекцией, что может привести к труднонаходимым багам и проблемам с производительностью. Даже если вы изначально ожидали стрим, подобное выражение может ввести в заблуждение тех, кто читает ваш код. А вот и типичный пример ловушки: Такой код выбросит IOException, сетующий на то, что стрим уже закрыт. Чтобы ясно обозначить наши намерения, лучше добавить toStream явно или, если нам после всего потребуется строгая коллекция, использовать toVector вместо toSeq. Последний подход чище и потенциально быстрее, так как внутри он использует единственный StringBuilder. Большинство советов для последовательностей так же хорошо работают и для множеств. Более того, для множеств есть несколько специфичных советов. Ранее мы уже ознакомились с этим правилом (на примере последовательностей), однако для множеств обоснование будет наиболее логичным. Метод sameElements может возвращать недетерминированные результаты для неупорядоченных коллекций, потому что этот метод принимает во внимание порядок элементов, на который мы не можем полагаться в случае с множествами.
Исключениями из правила будут классы, которые явно гарантируют предсказуемый порядок итерации: например, LinkedHashSet.
Также применимо к: Map. Так как Set[T] также явялется экземпляром Function1[T, Boolean], вы можете использовать его в этом качестве. При схожей производительности, последнее выражение будет яснее и выразительней.
Такое преобразование применимо и к последовательностям, однако стоит учесть, что в таком случае в работе с повторяющимися элементами потребуется особый подход. Опять же, при схожей производительности, обновленное выражение будет более ясными выразительным.
Потенциально, такое преобразование можно применить и к последовательностям, однако нам следует принять во внимание наличие дубликатов. Технически Option не является частью Scala коллекций, однако предоставляет похожий интерфейс (с монадическими методами и т.д.) и даже ведет себя как специальный тип коллекций, который может иметь, а может и не иметь какое-то значение. Многие из приведенных советов для последовательностей применимы и к Option. Кроме того, здесь представлены советы, характерные для Option API. При том, что сравнение является вполне законным, есть более простой способ, который позволяет проверить объявлен ли Option.
Еще одно преимущество данного упрощения в том, что если вы решите изменить тип от Option[T] к T, scalac скомпилирует предшествующее выражение (выдав только одно предупреждение), тогда как компиляция последнего справедливо закончится ошибкой. Этот совет дополняет предыдущий. В подобном трюкачестве нет нужды. Опять же, первое выражение и является корректным — оправдывать подобную экстравагантность не стоит. Более того, упрощенное выражение будет работать быстрее.
Также применимо к: Seq, Set. Причина та же, что и для последовательностей — простое свойство добавит меньше визуального шума, нежели составное выражение.
Заметьте, что у нас есть синонимы: isDefined (специфичный для option) и nonEmpty (специфичный для последовательностей). Возможно, было бы разумно отдать предпочтение первому для явного отделения Option и последовательностей. Для этого у нас есть более подходящий синтаксис. В этом случае мы можем полагаться на предопределенный метод, делая выражение короче. Можно выделить группы советов, связанные с тем, как обрабатываются значения Option. В документации, посвященной интерфейсу Option, говорится, что «самый идиоматичный способ использования экземпляра Option — это рассмотрение его в качестве коллекции или монады на ряду с использованием map, flatMap, filter или foreach». Основной принцип здесь заключается в том, чтобы избегать "check & get" (проверь и возьми) цепочек, которые обычно реализуются через оператор if или сопоставлением с образцом.
Цель — надежность, выразительность и «монадический» код: Это объяснение объединяет все последующие случаи. Приведенные выражения семантически эквиваленты (в обоих случаях z будет вычислен лениво — по требованию), однако последнее выражение короче. Преобразование может требовать дополнительного указания типа (из-за особенностей работы вывода типов в Scala), и в таких случаях предыдущее выражение предпочтительнее. Имейте в виду, что упрощение это весьма противоречиво из-за того, что последнее выражение выглядит менее ясно, особенно если вы к нему не привыкли. Мы представили довольно похожее правило для последовательностей (которое применимо и к Option). Нетипичное преобразование для вызова getOrElse. Последнее выражение смотрится чище. Для этого есть специальный метод, который делает это кратко и наименее затратно. Как и с другими типами коллекций, многие советы для последовательностей применимы и к таблицам, поэтому перечислим только характерные для таблиц. В принципе, первый фрагмент кода будет работать, однако производительность будет неоптимальной из-за того, что Map не является простой коллекцией пар (ключ, значение) — она может выполнять поиск куда более эффективным способом. Более того, последнее выражение проще и легче для понимания. Нет необходимости плодить промежуточный Option, когда необходимо сырое (raw) значение. Незачем рассматривать значение таблицы, как частичную функцию для получения опционального результата (что полезно для последовательностей), потому что у нас есть встроенный метод с такой же функциональностью. Хотя lift отлично работает, он выполняет дополнительное преобразование (от Map доPartialFunction) и может выглядеть весьма запутанным. Единственный вызов метода проще как синтаксически, так и с точки зрения производительности. В обоих случаях z вычисляется лениво, по требованию. Так как экземпляр Map[K, V] также является Function1[K, V], вы можете использовать его как функцию. Оптимизированные выражения являются более понятными (и потенциально более быстрыми). Упрощенные выражения понятней (и потенциально быстрее). Метод filterKeys обертывает исходную таблицу без копирования каких-либо элементов. В этом нет ничего плохого, однако вы вряд ли ожидаете от filterKeys подобного поведения. Поскольку оно неожиданно ведет так же, как представление, производительность кода может быть существенно снижена для некоторых случаев, например, для filterKeys(p).groupBy(???). Другой вероятной неприятностью является неожиданная «ленивость» (по умолчанию, фильтры коллекций должны быть строгими) – при вызове самого метода предикат вообще не вычисляется, из-за чего возможные побочные эффекты могут быть переупорядочены. Метод filterKeys, скорее всего, следовало бы объявить устаревшим, из-за невозможности сделать его строгим, не сломав обратную совместимость. Более подходящим именем для текущей реализации будет withKeyFilter (по аналогии с withFilter). В общем, наиболее разумно будет следовать Правилу наименьшего удивления и фильтровать ключи вручную. Тем не менее, поскольку схожая с представлением функциональность filterKeys потенциально полезна (когда доступ будет только к небольшому числу записей, в то время как таблица будет относительно большой), мы все же сможем рассмотреть возможность использования этого метода. Для того, чтобы не вводить в заблуждение людей, читающих (или модифицирующих) наш код, лучшим решением будет подчеркнуть наши намерения, объявив подходящий синоним: Мы используем псевдоним типа MapView для того, чтобы обозначить, что результирующая таблица является view-подобной. Другим вариантом будет объявление простого вспомогательного метода: Обоснование такое же, как и в предыдущем случае. Аналогичным способом мы можем объявить недвусмысленный синоним: Проще объявить вспомогательный метод вроде: Мы можем полагаться на упрощенный синтаксис, чтобы отфильтровать ключи. Также, как и с последовательностями, мы можем полагаться на синтаксический сахар для упрощения подобных операторов. В дополнение к приведенным рецептам я рекомендую вам посмотреть на официальную документацию библиотеки коллекций Scala, которую на удивление легко читать. Смотрите также: Эти головоломки оказали мне неоценимую помощь в формировании и углублении понимания коллекций Scala. Не смотря на обширность вышеприведенного списка, он наверняка далек от завершения. Более того, из-за сильно варьирующейся применимости этих рецептов, некоторая шлифовка определенно потребуется. Ваши предложения приветствуются. Большое спасибо Павлу Фатину за разрешение на перевод статьи. Спасибо Ледовских Владу за вычитку перевода (этот абзац никто не вычитывал). Благодарю firegurafiku за ценные советы и рекомендации, которые помогли мне в переводе. Lead software engineer