Skip to content

Latest commit

 

History

History
158 lines (117 loc) · 9.83 KB

p02-seq-extractors.md

File metadata and controls

158 lines (117 loc) · 9.83 KB

Глава 2: Извлекаем коллекции значений

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

К примеру, мы можем определить образец, который представляет только списки из двух элементов, или образец, который представляет только списки из трёх элементов:

val xs = 3 :: 6 :: 12 :: Nil
xs match {
  case List(a, b) => a * b
  case List(a, b, c) => a + b + c
  case _ => 0
}

Также мы можем представить списки, размер которых нам заранее неизвестен, с помощью оператора _*:

val xs = 3 :: 6 :: 12 :: 24 :: Nil
xs match {
  case List(a, b, _*) => a * b
  case _ => 0
}

Здесь сопоставление с образцом проходит успешно. В первой case-альтернативе происходит связывание переменных a и b, остаток списка отбрасывается. Нам не важно, сколько элементов осталось в списке.

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

Как раз для этого и предназначен метод unapplySeq. Посмотрим на возможные сигнатуры типов:

def unapplySeq(object: S): Option[Seq[T]]

Он принимает на вход значение типа S и возвращает либо None, если значение совсем не подходит, или коллекцию значений некоторого типа T, обёрнутую в Some.

Пример: извлечение полного имени

Давайте поупражняемся с новым типом экстракторов, пусть и на несколько надуманном примере. Предположим, что в нашем приложении нам необходимо извлечь имя пользователя. В английском языке имя может состоять из нескольких имён, например: "Daniel", "Catherina Johanna", или "Matthew John Michael". Нам бы хотелось уметь извлекать все части составного имени.

Вот очень простой экстрактор, определённый с помощью метода unapplySeq, который как раз этим и занимается:

object GivenNames {
  def unapplySeq(name: String): Option[Seq[String]] = {
    val names = name.trim.split(" ")
    if (names.forall(_.isEmpty)) None else Some(names)
  }
}

Если строка содержит одно или несколько имён, экстрактор извлечёт всю последовательность имён. Если строка не содержит ни одного имени, экстрактор вернёт None и строка не пройдёт соответствие с образцом.

Давайте протестируем наш новый экстрактор:

def greetWithFirstName(name: String) = name match {
  case GivenNames(firstName, _*) => "Good morning, " + firstName + "!"
  case _ => "Welcome! Please make sure to fill in your name!"
}

Этот изящный метод возвращает приветствие для полного имени, извлекая лишь первую часть. Так greetWithFirstName("Daniel") вернёт "Good morning, Daniel!", в то время как, вызвав greetWithFirstName("Catherina Johanna"), мы получим "Good morning, Catherina!".

Совместное применение экстракторов с фиксированным и переменным числом параметров

Иногда нам хочется извлечь определённый набор значений, число которых известно на этапе компиляции, а также дополнительный набор значений, число которых заранее неизвестно, их может и не быть вовсе.

Предположим, что нам нужно извлечь полное имя. Оно содержит имя человека (возможно составное), а также и фамилию, к примеру: "John Doe" или "Catherina Johanna Peterson". Мы хотим связать фамилию с первой переменной образца, основное имя — со второй переменной и с третьей коллекцию из оставшихся имён.

Для этого воспользуемся другим вариантом метода unapplySeq:

def unapplySeq(object: S): Option[(T1,  .., Tn-1, Seq[T])]

Как видно из сигнатуры, метод unapplySeq также может возвращать кортеж значений, в котором последний элемент должен быть коллекцией типа Seq. Этот вариант очень похож на то, что мы видели в предыдущей главе для метода unapply.

Посмотрим на определение экстрактора:

object Names {
  def unapplySeq(name: String): Option[(String, String, Seq[String])] = {
    val names = name.trim.split(" ")
    if (names.size < 2) None
    else Some((names.last, names.head, names.drop(1).dropRight(1)))
  }
}

Присмотритесь повнимательней к типу возвращаемого значения. Обратите внимание на то, как мы построили кортеж в конструкторе Some. Мы передали в него кортеж Tuple3, построенный с помощью специального синтаксиса для кортежей. Мы просто заключили три элемента — фамилию, имя и список дополнительных имён в круглые скобки и разделили их запятыми (компилятор Scala передаст значения в конструктор Tuple3 за нас).

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

Воспользуемся нашим экстрактором для приветствия пользователя:

def greet(fullName: String) = fullName match {
  case Names(lastName, firstName, _*) => "Good morning, " + firstName + " " + lastName + "!"
  case _ => "Welcome! Please make sure to fill in your name!"
}

Поэкспериментируйте с этим примером в интерпретаторе или в интерактивной странице IDE (worksheet).

Итоги

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

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

Обновление, 24.01.2013: Я поправил пример для экстрактора GivenNames, спасибо Christophe Bliard за то, что указал на ошибку.