web/ru/collections.textile (284 lines of code) (raw):

--- prev: basics2.textile next: pattern-matching-and-functional-composition.textile title: Коллекции layout: post --- В этом уроке вы узнаете: * Основные структуры данных ** "Списки":#Lists ** "Наборы":#Sets ** "Кортеж":#Tuple ** "Карты":#Maps * Функциональные Комбинаторы ** "map":#map ** "foreach":#foreach ** "filter":#filter ** "zip":#zip ** "partition":#partition ** "find":#find ** "drop и dropWhile":#drop ** "foldRight и foldLeft":#fold ** "flatten":#flatten ** "flatMap":#flatMap ** "Обобщенные функциональные комбинаторы":#generalized ** "А как же Map?":#vsMap h1. Основные структуры данных В Scala существует несколько прекрасных коллекций. *Смотрите также:* В Effective Scala описывается как использовать <a href="https://twitter.github.com/effectivescala/index-ru.html#Коллекции">Коллекции</a>. h2(#Lists). Списки <pre> scala> val numbers = List(1, 2, 3, 4) numbers: List[Int] = List(1, 2, 3, 4) </pre> h2(#Sets). Наборы Наборы не содержат одинаковых элементов <pre> scala> Set(1, 1, 2) res0: scala.collection.immutable.Set[Int] = Set(1, 2) </pre> h2(#Tuple). Кортеж Кортеж объединяет простые логические элементы коллекции без использования классов. <pre> scala> val hostPort = ("localhost", 80) hostPort: (String, Int) = (localhost, 80) </pre> В отличие от case классов, у него нет именованных функций доступа, вместо этого у него есть функции доступа, которые носят названия по номеру позиции элемента кортежа и они нумеруются с 1, а не с 0. <pre> scala> hostPort._1 res0: String = localhost scala> hostPort._2 res1: Int = 80 </pre> Кортежи прекрасно могут использоваться вместе с сопоставлением с образцом. <pre> hostPort match { case ("localhost", port) => ... case (host, port) => ... } </pre> Кортежи имеет специальный "соус", который позволяет сделать Кортеж 2 значений: <code>-></code> <pre> scala> 1 -> 2 res0: (Int, Int) = (1,2) </pre> *Смотрите также:* В Effective Scala описывается <a href="https://twitter.github.com/effectivescala/index-ru.html#Функциональное программирование-Разрушение связей (Destructuring bindings)">разрушение связей</a> ("распаковка" кортежа). h2(#Maps). Карты Они могут содержать в себе основные типы данных. <pre> Map(1 -> 2) Map("foo" -> "bar") </pre> Выглядит словно это особый синтаксис, но вспомните наше обсуждение в разделе Кортеж, что с помощью <code>-></code> можно создавать Кортежи. Map() тоже использует синтаксис, который мы изучили ранее на Уроке №1: <code>Map(1 -> "one", 2 -> "two")</code>, который раскрывается в <code>Map((1, "one"), (2, "two"))</code>, где первый элемент является ключом, а вторым элементом является некое значение. Карты внутри себя могут содержать другие Карты или даже функции. <pre> Map(1 -> Map("foo" -> "bar")) </pre> <pre> Map("timesTwo" -> { timesTwo(_) }) </pre> h2(#Option). Опция <code>Опция</code> представляет собой контейнер, который хранит какое-то значение или не хранит ничего совсем. Основной интерфейс Опции выглядит следующим образом: <code> trait Option[T] { def isDefined: Boolean def get: T def getOrElse(t: T): T } </code> Опция сам по себе это обобщенный тип и он имеет два подкласса: <code>Some[T]</code> и <code>None</code> Давайте взглянем на пример того, как Опция может использоваться: <code>Map.get</code> использует <code>Option</code> для возврата собственных значений. Опция говорит вам, что метод может не вернуть того значения, которое мы запросили. <pre> scala> val numbers = Map("one" -> 1, "two" -> 2) numbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2) scala> numbers.get("two") res0: Option[Int] = Some(2) scala> numbers.get("three") res1: Option[Int] = None </pre> Теперь наши данные отлавливаются с помощью <code>Option</code>. Как мы можем это использовать? Первое, что приходит на ум, это использовать условие, опираясь на метод <code>isDefined</code>. <code> // Мы хотим умножить число на 2 или возвратить 0. val result = if (res1.isDefined) { res1.get * 2 } else { 0 } </code> Мы предполагаем, что вы будете использовать <code>getOrElse</code> или сопоставление с образцом для результата. <code>getOrElse</code> дает вам легкий способ объявить стандартное значение. <code> val result = res1.getOrElse(0) * 2 </code> <code>Option</code> прекрасно работает вместе с сопоставлением с образцом. <code> val result = res1 match { case Some(n) => n * 2 case None => 0 } </code> *Смотрите также:* В Effective Scala описываются <a href="https://twitter.github.com/effectivescala/index-ru.html#Функциональное программирование-Опция">Опции</a>. h1. Функциональные комбинаторы Комбинаторы называются так потому, что они созданы, чтобы объединять результаты. Результат одной функции часто используется в качестве входных данных для другой. Наиболее распространенным способом, является использование их со стандартными структурами данных. h2(#map). map Применяет функцию к каждому элементу из списка, возвращается список с тем же числом элементов. <pre> scala> numbers.map((i: Int) => i * 2) res0: List[Int] = List(2, 4, 6, 8) </pre> или передается частично вызываемая функция <pre> scala> def timesTwo(i: Int): Int = i * 2 timesTwo: (i: Int)Int scala> numbers.map(timesTwo _) res0: List[Int] = List(2, 4, 6, 8) </pre> h2(#foreach). foreach foreach похож на map, но ничего не возвращает. foreach используется для создания побочных эффектов. <pre> scala> numbers.foreach((i: Int) => i * 2) </pre> Он ничего не возвращает. Вы можете попробовать сохранить результат в переменную, но она будет иметь тип Unit (другими словами void) <pre> scala> val doubled = numbers.foreach((i: Int) => i * 2) doubled: Unit = () </pre> h2(#filter). filter Данный комбинатор удаляет любой элемент, если функция, применяемая к этому элементу, возвращает ложь. Функции, которые возвращают Boolean, часто называются функциями-предикатами <pre> scala> numbers.filter((i: Int) => i % 2 == 0) res0: List[Int] = List(2, 4) </pre> <pre> scala> def isEven(i: Int): Boolean = i % 2 == 0 isEven: (i: Int)Boolean scala> numbers.filter(isEven _) res2: List[Int] = List(2, 4) </pre> h2(#zip). zip zip объединяет содержимое двух списков в один парный список. <pre> scala> List(1, 2, 3).zip(List("a", "b", "c")) res0: List[(Int, String)] = List((1,a), (2,b), (3,c)) </pre> h2(#partition). partition <code>partition</code> разделяет список, в зависимости от результата, который возвращает функция-предикат. <pre> scala> List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).partition(_ % 2 == 0) res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9)) </pre> h2(#find). find find возвращает первый элемент коллекции, который удовлетворяет функции-предикату. <pre> scala> numbers.find((i: Int) => i > 5) res0: Option[Int] = Some(6) </pre> h2(#drop). drop и dropWhile <code>drop</code> удаляет первые i элементов <pre> scala> numbers.drop(5) res0: List[Int] = List(6, 7, 8, 9, 10) </pre> <code>dropWhile</code> удаляет первый элемент, который не удовлетворяет функции-предикату. Например, если мы применим <code>dropWhile</code> к нечетным числам из нашего списка, то <code>1</code> будет удалена (но не <code>3</code>, которая стоит за <code>2</code>). <pre> scala> numbers.dropWhile(_ % 2 != 0) res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10) </pre> h2(#fold). foldLeft <pre> scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n) res0: Int = 55 </pre> 0 - это начальное значение (Не забывайте, что мы используем числа из List[Int]), где m работает как аккумулятор. Взгляните сами: <pre> scala> numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n } m: 0 n: 1 m: 1 n: 2 m: 3 n: 3 m: 6 n: 4 m: 10 n: 5 m: 15 n: 6 m: 21 n: 7 m: 28 n: 8 m: 36 n: 9 m: 45 n: 10 res0: Int = 55 </pre> h3. foldRight Этот комбинатор похож на foldLeft, за исключением того, что он работает с противоположной стороны. <pre> scala> numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n } m: 10 n: 0 m: 9 n: 10 m: 8 n: 19 m: 7 n: 27 m: 6 n: 34 m: 5 n: 40 m: 4 n: 45 m: 3 n: 49 m: 2 n: 52 m: 1 n: 54 res0: Int = 55 </pre> h2(#flatten). flatten flatten сжимает вложенные структуры. <pre> scala> List(List(1, 2), List(3, 4)).flatten res0: List[Int] = List(1, 2, 3, 4) </pre> h2(#flatMap). flatMap flatMap это часто используемый комбинатор, который объединяет map и flatten. flatMap берет функцию, которая работает с вложенными списками и объединяет результаты. <pre> scala> val nestedNumbers = List(List(1, 2), List(3, 4)) nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4)) scala> nestedNumbers.flatMap(x => x.map(_ * 2)) res0: List[Int] = List(2, 4, 6, 8) </pre> Думайте об этом, как о коротком способе использования map, а затем применения flatten к результату: <pre> scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flatten res1: List[Int] = List(2, 4, 6, 8) </pre> в этом примере вызывается map, а позднее flatten, как пример "комбинаторной" природы этих функций. *Смотрите также:* В Effective Scala описывается <a href="https://twitter.github.com/effectivescala/index-ru.html#Функциональное программирование-`flatMap`">flatMap</a>. h2(#generalized). Обобщенные функциональные комбинаторы Теперь мы узнали о множестве функции для работы с коллекциями. Что нам может понадобиться, чтобы иметь возможность написать свои функциональные комбинаторы? Интересно, что каждый функциональный комбинатор показанный выше, может быть написан поверх fold. Рассмотрим несколько примеров. <pre> def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = { numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) => fn(x) :: xs } } scala> ourMap(numbers, timesTwo(_)) res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20) </pre> Почему именно <tt>List[Int]()</tt>? Scala не достаточно умна, чтобы реализовать пустой список, для хранения целочисленных значений. h2(#vsMap). А как же Map? Все функциональные комбинаторы прекрасно работают и с Картами. Карты можно рассматривать как список пар, так что функции, которые вы пишете работают с парами ключей и значений Карты. <pre> scala> val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201) extensions: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101), (joe,201)) </pre> теперь выберем каждую запись, у которой телефонный код меньше 200. <pre> scala> extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200) res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101)) </pre> Т.к. в результате вы получаете кортеж, то вам приходится тащить ключи и значения с их позиционными функциями доступа. Да уж! К счастью, мы можем использовать сопоставление с образцом, чтобы извлечь ключ и значение. <pre> scala> extensions.filter({case (name, extension) => extension < 200}) res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101)) </pre> Почему это работает? Почему вы можете использовать частичный вызов функции? Следите за обновлениями на следующей неделе!