Путеводитель по Scala для Java-разработчиков: Коллекции

Работа с кортежами, массивами и списками в Scala

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

Тед Ньювард, Глава, Neward & Associates

Тед Ньювард - глава Neward & Associates, где он консультирует, руководит, обучает и внедряет Java, .NET, XML Services и другие платформы. Он проживает возле Сиэтла, штат Вашингтон.



21.05.2009

С точки зрения Java™-разработчика, знакомство со Scala логично начинать с объектов. В предыдущих статьях серии рассказывалось об объектно-ориентированных аспектах программирования на Scala, которые во многом схожи с Java. Кроме того, было показано, как Scala возвращается к базовым концепциям объектно-ориентированного дизайна, переосмысливает их и предлагает разработчикам в новом, современном исполнении. При этом мы практически не затрагивали один принципиально важный момент, а именно то, что Scala – это функциональный язык программирования (в отличие от множества нефункциональных языков).

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

Обнуляемые типы в C# 2.0

В других языках предпринимались различные попытки решения проблемы с пустыми значениями. В С++ эта проблема фактически игнорировалась в течение долгого времени, пока в конце концов не было решено, что null и ноль – это все же разные вещи. В Java по-прежнему существуют сложности, несмотря на так называемый "автобоксинг" – автоматическую конвертацию значений примитивных типов в соответствующие объекты (кстати, последние были предложены только в версии 1.1). Некоторые ярые сторонники паттернов ратуют за то, чтобы каждый класс включал так называемый Null-объект – специальный экземпляр (на самом деле, дочерний класс), который бы перегружал все методы базового типа, не выполняя в них никаких действий. Однако на практике такое решение заставляет программиста делать много лишней работы. Принципиально новый подход к решению проблемы пустых значений был выбран разработчиками C# после выхода C# 1.0.

В C# 2.0 появилось понятие обнуляемых типов (nullable types). Фактически решение представляет собой синтаксическую поддержку значения null во всех примитивных типах путем заключения их внутрь специального шаблонного класса Nullable<T>. Эта поддержка декларируется неявно при помощи добавления модификатора ? в объявление типа. Например, int? означает целочисленный тип, допускающий значение null.

На первый взгляд, это может показаться разумным подходом, однако, если копнуть глубже, сразу же выявляются сложности. Например, должна ли поддерживаться совместимость между типами int и int? и если да, то каким образом int должен преобразовываться к int? и наоборот? Что должно происходить при добавлении значения int к int? и должен ли результирующий тип данных допускать значение null? Подобных вопросов можно задать очень много. Из-за этого, несмотря на все изменения в системе типов C#, которые позволили включить обнуляемые типы в релиз C# 2.0, они практически полностью игнорируются разработчиками.

Оглядываясь назад, можно сказать, что вариант с типом Option, который позволяет четко разделить типы Option[T] и Int, выглядит проще и логичнее остальных, особенно в сравнении с некоторыми запутанными правилами преобразования, которые сопутствуют системе обнуляемых типов. Не забывайте, что разработчики функциональных языков размышляли над этой проблемой почти два десятка лет. Таким образом, несмотря на то, что для привыкания к Option[T] требуется некоторое время, он позволяет писать более "чистый" и понятный код в общем случае.

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

Работа с типом Option

В каких случаях "ничего" действительно означает отсутствие чего-либо? Ответ прост: "ничего" – это ноль объектов, а не специальное значение null.

Понятием "ничего" (nothing) оказывается на удивление непросто оперировать при разработке программного обеспечения, особенно если учесть, что оно интуитивно ясно для большинства из нас. Например, посмотрите, какие жаркие споры идут в сообществе C++ на тему того, что лучше использовать – NULL или 0. Аналогичная ситуация наблюдается с SQL насчет допустимости значений NULL в столбцах базы данных. Большинство программистов под "ничего" понимают NULL (или null, если хотите), однако при программировании на Java это таит в себе определенные проблемы.

Представьте себе простую ситуацию, при которой необходимо извлечь данные о зарплате программиста из некоторой базы данных, находящейся в памяти или на диске. API спроектирован таким образом, чтобы принимать на вход строковое значение (имя программиста). При этом неясно, значение какого типа должно возвращаться. С точки зрения представления предметной области, возвращаться должно значение типа Int – годовой оклад программиста, но что делать, если он отсутствует в базе данных (он мог быть уже уволен, еще не нанят, либо в имя закралась опечатка)? Если API возвращает значение типа Int, то в этом случае мы не можем использовать null – типичный флаг, сигнализирующий об отсутствии информации в базе данных. Конечно, можно выбрасывать исключение, однако отсутствие данных отнюдь не является примером исключительной ситуации, поэтому этот вариант не совсем подходит.

В Java для решения этой проблемы можно использовать тип java.lang.Integer, явно указывающий внешнему коду на то, что метод может потенциально вернуть null. При этом нам остается только надеяться на то, что одни программисты детально опишут этот момент в документации, а другие ее внимательно прочитают. Примерно с таким же успехом можно надеяться, что менеджеры будут внимательно слушать наши возражения на тему нереальных сроков разработки, а также на то, что они примут их во внимание и объяснят вышестоящему руководству и клиентам.

Scala предлагает выход из этой тупиковой ситуации, который характерен для функциональных языков. В ряде случаев тип Option или Option[T] даже не описывается. Это класс общего назначения, который имеет ровно два дочерних класса – Some[T] и None, при помощи которых можно корректно декларировать возможность отсутствия какого-либо значения, причем, не требуя при этом значительных усилий на уровне системы типов языка. Далее мы рассмотрим пример использования типа Option[T], который должен помочь прояснить основные моменты.

При работе с Option[T] самое главное – это отдавать себе отчет в том, что он представляет собой строго типизированную коллекцию единичного размера, в которой для представления возможности отсутствия значения используется специальный объект None. Таким образом, вместо метода, возвращающего null при отсутствии данных, наш метод будет возвращать значение типа Option[T], где T – это тип искомого объекта. Далее, при отсутствии информации метод будет возвращать None, как показано в листинге 1.

Листинг 1. Готовы сыграть в футбол?
  @Test def simpleOptionTest =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins",
          "Los Angeles" -> null)
    
    assertEquals(footballTeamsAFCEast.get("Miami"), Some("Dolphins"))
    assertEquals(footballTeamsAFCEast.get("Miami").get(), "Dolphins")
    assertEquals(footballTeamsAFCEast.get("Los Angeles"), Some(null))
    assertEquals(footballTeamsAFCEast.get("Sacramento"), None)
  }

Обратите внимание, что метод get класса Map возвращает не само значение, соответствующее переданному ключу, а экземпляр Option[T], который представляет собой либо обертку Some() вокруг требуемого значения, либо None, что означает отсутствие ключа в массиве. Такой подход является особенно выигрышным, если массив допускает хранение значений null, которое, например, соответствует ключу Los Angeles в листинге 1.

При работе с Option[T] разработчики в большинстве случае применяют метод сопоставления с образцом (pattern matching). Это чисто функциональный метод, который позволяет организовывать логическое ветвление (аналогично оператору "switch") на основе информации о типах или значениях, а также связывать значения с переменными при их объявлении, проверять, является ли значение экземпляром Some() или None, и извлекать внутреннее значение Some без необходимости вызывать устаревший метод get(). Пример использования сопоставления с образцом в Scala приведен в листинге 2.

Листинг 2. Типичный пример сопоставления с образцом
  @Test def optionWithPM =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins")
          
    def show(value : Option[String]) =
    {
      value match
      {
        case Some(x) => x
        case None => "No team found"
      }
    }
    
    assertEquals(show(footballTeamsAFCEast.get("Miami")), "Dolphins")
  }

Кортежи и множества

В C++ они называются структурами. В Java – более известны как объекты передачи данных (DTO) или объекты-параметры. В Scala они получили название кортежи (tuples). Так или иначе, они представляют собой классы, объединяющие в себе набор значений других типов данных, при этом не реализующие практически никаких механизмов инкапсуляции или абстракции (более того, как правило, желательно, чтобы в подобных классах не было никаких абстракций).

Типы-кортежи создаются в Scala до смешного просто, причем идея заключается в том, что не имеет никакого смысла придумывать отдельное имя для типа, который полностью описывается содержащимися в нем элементами, если они являются открытыми и доступными извне. Пример приведен в листинге 3.

Листинг 3. tuples.scala
// Набор тестов JUnit
//
class TupleTest
{
  import org.junit._, Assert._
  import java.util.Date
 
  @Test def simpleTuples() =
  {
    val tedsStartingDateWithScala = Date.parse("3/7/2006")

    val tuple = ("Ted", "Scala", tedsStartingDateWithScala)
    
    assertEquals(tuple._1, "Ted")
    assertEquals(tuple._2, "Scala")
    assertEquals(tuple._3, tedsStartingDateWithScala)
  }
}

Для создания кортежа достаточно поместить необходимые значения в круглые скобки – практически так же, как если бы это был вызов метода. Для извлечения значений из кортежа служит метод _n, где n задает номер позиции элемента (_1 означает первый элемент кортежа, _2 – второй и т. д.). Таким образом, экземпляр стандартного класса java.util.Map можно представить себе как набор кортежей из двух элементов.

Кортежи представляют собой простой способ организации нескольких элементов данных в единый объект. В частности, они позволяют с легкостью возвращать несколько значений из метода, что в программировании на Java требует значительных усилий. Рассмотрим простой пример: необходим метод для подсчета числа вхождений всех символов в строке и нахождения наиболее часто встречающегося символа. При этом требуется возвращать не только сам символ, но и число его вхождений. Это серьезно усложняет дизайн метода, так как приходится либо создавать отдельный класс для хранения пары значений, либо сохранять их в полях класса метода, которые затем могут быть опрошены отдельно. Так или иначе, реализация выливается во множество строк кода, особенно по сравнению с фрагментом кода на Scala, в котором просто возвращается кортеж, содержащий сам символ и число его вхождений. Кроме того, Scala позволяет легко обращаться к значениям кортежа (при помощи методов типа "_1", "_2" и т. д.), поэтому задача возвращения нескольких значений не представляет никаких сложностей.

Как вы узнаете в следующем разделе, программисты Scala часто хранят кортежи и объекты типа Option в коллекциях, в частности, в списках и экземплярах Array[T]. Благодаря этому такие сравнительно простые синтаксические конструкции приобретают солидную мощь и гибкость.


Массивы в Scala

Вначале обратимся к старым знакомым – массивам, которые в Scala представлены типом Array[T]. Подобно массивам в Java, экземпляры Array[T] представляют собой упорядоченные наборы элементов, причем индексом каждого элемента является его позиция в наборе. Индекс не может превышать длину массива (листинг 4).

Листинг 4. array.scala
object ArrayExample1
{
  def main(args : Array[String]) : Unit =
  {
    for (i <- 0 to args.length-1)
    {
      System.out.println(args(i))
    }
  }
}

Несмотря на совместимость с массивами в Java, которая, в частности, означает, что они преобразуются в Java-массивы при компиляции в байт-код, массивы в Scala определяются по-другому. Для новичков поясним, что Scala-массивы – это параметризованные классы, которые отнюдь не являются некими встроенными типами (по крайней мере, не в большей степени, чем остальные классы стандартной библиотеки Scala). Они являются экземплярами Array[T] – класса, который предоставляет ряд интересных методов, в том числе "length", который, как и следует из названия, возвращает длину массива. Таким образом, Scala поддерживает классический способ обхода элементов массива, т. е. путем перебора всех элементов, начиная с нулевого индекса и заканчивая индексом args.length - 1. При этом для обращения к i-му элементу массива достаточно поместить его индекс в круглые (а не квадратные) скобки. Кстати говоря, круглые скобки – это еще один пример метода с "забавным именем".

Однако этот метод не очень интересен (а самое главное – он не следует функциональному стилю программирования).

Совет

Полностью иерархия Array[T] описана в Scaladocs. Она выглядит весьма привлекательно и во многом напоминает пакет java.util Collections.

Расширение массива

На самом деле, класс Array обладает большим числом методов, которые он унаследовал от удивительно развитой иерархии базовых типов. Array расширяет Array0, который является наследником ArrayLike[A], расширяющего Mutable[A], который в свою очередь является дочерним по отношению к RandomAccessSeq[A] – наследнику Seq[A], и т. д. На практике это просто означает, что Array предоставляет множество методов, облегчающих работу с массивами в Scala (особенно в сравнении с массивами в Java).

Например, перебор элементов массива (см. листинг 4) можно осуществлять значительно проще и в более "функциональной" манере, используя метод foreach, унаследованный от признака Iterable. При этом, если простоту такого подхода еще можно оспорить, то его принадлежность к функциональному программированию совершенно очевидна. Пример приведен в листинге 5.

Листинг 5. Второй пример работы с массивами
object 
{
  def main(args : Array[String]) : Unit =
  {
    args.foreach( (arg) => System.out.println(arg) )
  }
}

На первый взгляд может показаться, что этот вариант ничем не проще итерирования при помощи индекса. Однако возможность передачи функции (именованной или анонимной) в другой класс для выполнения определенных действий – в данном случае, для итерирования – является ключевой в функциональном программировании. Подобное использование функций высшего порядка никоим образом не ограничивается перебором элементов массива, например, таким образом можно сначала организовать некоторую фильтрацию содержимого, отбрасывая нежелательные объекты, а затем уже обрабатывать массив. Для решения этой задачи в Scala логично использовать метод filter для фильтрации, а затем обработать оставшееся содержимое при помощи либо функции map и другой функции (типа (T) => U, где T и U – вновь параметризованные типы), либо метода foreach. Второй вариант иллюстрируется в листинге 6. Обратите внимание, что метод filter принимает на вход метод типа (T) : Boolean. Этот тип означает, что метод должен получать на вход единственный параметр, тип которого соответствует типу элементов массива, и возвращать экземпляр Boolean.

Листинг 6. Поиск всех Scala-разработчиков
class ArrayTest
{
  import org.junit._, Assert._
  
  @Test def testFilter =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "Groovy", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#")),
    new Person("Scott", "Davis", 40, 50000,
      Array("Java", "Groovy"))
      )

    // Найти всех разработчиков на Scala ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Их должно быть 2
    assertEquals(2, scalaProgs.length)
    
    // ... и выполнить некоторую операцию над 
    // полученным массивом Scala-разработчиков (повысить им зарплату, разумеется!)
    //
    scalaProgs.foreach((p) => p.salary += 5000)
    
    // Увеличить зарплату каждого на 5000 ...
    assertEquals(programmers(0).salary, 50000 + 5000)
    assertEquals(programmers(1).salary, 45000 + 5000)
    
    // ... кроме тех, кто не знает Scala
    assertEquals(programmers(2).salary, 45000)
  assertEquals(programmers(3).salary, 50000)
  }
}

Функция map часто используется в случаях, когда необходимо создать новый массив, сохранив исходный массив неизменным. Как правило, большинство программистов на функциональных языках предпочитают именно так работать с массивами (листинг 7).

Листинг 7. Сочетание filter и map
  @Test def testFilterAndMap =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#"))
    new Person("Scott", "Davis", 40, 50000,
      Array("Java", "Groovy"))
      )

    // Найти всех разработчиков на Scala ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Их должно быть 2
    assertEquals(2, scalaProgs.length)
    
    // ... и выполнить некоторую операцию над 
    // полученным массивом Scala-разработчиков (повысить им зарплату, разумеется!)
    //
    def raiseTheScalaProgrammer(p : Person) =
    {
      new Person(p.firstName, p.lastName, p.age,
        p.salary + 5000, p.skills)
    }
    val raisedScalaProgs = 
      scalaProgs.map(raiseTheScalaProgrammer)
    
    assertEquals(2, raisedScalaProgs.length)
    assertEquals(50000 + 5000, raisedScalaProgs(0).salary)
    assertEquals(45000 + 5000, raisedScalaProgs(1).salary)
  }

Обратите внимание, что в листинге 7 поле salary (зарплата) класса Person может быть объявлена при помощи ключевого слова val, а не var как раньше, когда данное поле приходилось модифицировать при изменении оклада.

Класс Array в Scala предоставляет больше методов, чем можно описать в рамках данной статьи. Старайтесь максимально задействовать их при работе с массивами вместо того чтобы использовать традиционный метод (т. е. при помощи оператора for ...) для нахождения нужного элемента массива. Как правило, легче всего бывает создать функцию, выполняющую нужные действия над элементами, а затем передавать ее в функции map, filter, foreach или другие методы Array, в зависимости от задачи. При этом функцию можно создавать непосредственно внутри другого метода, как показано в листинге 7.


Функциональные забавы со списками

Списки уже многие годы являются фундаментальной конструкцией в функциональных языках. По сути, они представляют собой настолько же стандартную структуру данных в функциональном программировании, как массивы – в объектно-ориентированном. Они играют ключевую роль при создании функциональных приложений, поэтому, если вы хотите освоить Scala, то вам придется изучить их и принципы их работы. Даже если вы не используете их напрямую, то они все равно активно используются внутри библиотек Scala. Поэтому изучение списков является... как бы лучше выразиться... императивным, т. е. обязательным (по аналогии с императивным программированием).

(Приношу свои извинения за специфический программистский юмор. Больше не повторится).

В Scala списки схожи с массивами в том, что их базовый класс – List[T] - входит в стандартную библиотеку Scala. Кроме того, подобно Array[T], List[T] является наследником множества базовых классов и признаков, начиная с класса Seq[T].

С принципиальной точки зрения, списки представляют собой коллекции элементов, доступ к которым осуществляется либо через начальный, либо через конечный объект в списке. Списки получили распространение благодаря языку Lisp, причем его ярые приверженцы наверняка помнят, что это название произошло от сокращения "LISt Processing" (обработка списков). Начальный элемент списка в Lisp можно было получить при помощи операции car, а конечный – cdr. Кстати говоря, у этих имен есть историческое обоснование. Первый, кто его отгадает и пришлет мне правильный ответ, получит приз.

Во многом со списками работать легче, чем с массивами, так как, во-первых, Scala является функциональным языком, а они исторически предоставляли мощную поддержку списков, а во-вторых, списки легче создавать и декомпозировать. Рассмотрим в качестве примера функцию, которая должна декомпозировать список на отдельные элементы. Для этого ей достаточно взять первый элемент – "вершину" списка – обработать его, а затем вызвать саму себя рекурсивным образом, передав на вход остальную часть списка. Такой подход резко снижает вероятность того, что внутри функции возникнет некоторое внутреннее состояние, от которого будет зависеть обработка. Кроме того, это предоставляет дополнительные возможности по распараллеливанию выполнения функции по нескольким потокам, поскольку на каждом шаге происходит обработка только одного элемента. Это особенно важно при выполнении сложных процедур обработки списков.

Создание списков и расщепление их на элементы представляют собой весьма тривиальные операции. Пример приведен в листинге 8.

Листинг 8. Пример работы со списками
class ListTest
{
  import org.junit._, Assert._
  
  @Test def simpleList =
  {
    val myFirstList = List("Ted", "Amanda", "Luke")
    
    assertEquals(myFirstList.isEmpty, false)
    assertEquals(myFirstList.head, "Ted")
    assertEquals(myFirstList.tail, List("Amanda", "Luke")
    assertEquals(myFirstList.last, "Luke")
  }
}

Как видите, создание списка аналогично созданию массива – синтаксис похож на конструирование обычного объекта за тем исключением, что не используется ключевое слово "new" (это одна из характерных черт так называемых "case-классов", которые будут рассматриваться в следующих статьях). Обратите особое внимание на результат вызова метода tail – он возвращает отнюдь не последний элемент списка (для этого служит метод last), а весь список за исключением его вершины.

Разумеется, что мощь списков во многом базируется на возможности рекурсивной обработки элементов. Другими словами, можно просто выбирать и обрабатывать вершину списка до тех пор, пока он не станет пустым, а затем собирать результаты воедино. Пример показан в листинге 9.

Листинг 9. Пример рекурсивной обработки списка
  @Test def recurseList =
  {
    val myVIPList = List("Ted", "Amanda", "Luke", "Don", "Martin")
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

Учтите, что компилятор или интерпретатор Scala выдаст сообщение об ошибке, если забыть про объявление типа возвращаемого значения метода count. Этот метод работает по принципу хвостовой рекурсии, поэтому указание типа возвращаемого значения необходимо для оптимизации его работы, в частности, для снижения числа выделяемых стековых фреймов при рекурсивных вызовах. Разумеется, для получения числа элементов было бы легче вызывать метод "length" класса List, однако смысл примера в том, чтобы показать всю мощь рекурсивных принципов обработки списков. Метод, показанный в листинге 9, абсолютно потокобезопасен, так как все промежуточное состояние при его выполнении представляется в виде параметров в стеке, поэтому одновременный доступ к нему из нескольких параллельных потоков невозможен по определению. Одной из привлекательных особенностей функционального программирования является то, что достаточно трудно писать программы в функциональном стиле и при этом создавать объекты, обладающие состоянием, которое может быть изменено извне.

API для работы со списками

У списков есть ряд интересных свойств, в частности, их можно создавать при помощи метода :: (да-да, это очередной метод с "забавным именем"). Таким образом, вместо вызова конструктора класса List можно просто "сцеплять" элементы (от английского "cons", обозначающего оператор ::). Пример приведен в листинге 10.

Листинг 10. Пример создания списка
  @Test def recurseConsedList =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

Будьте внимательны при использовании метода ::, поскольку с ним связано несколько непривычных правил. Эта языковая конструкция настолько широко распространена в функциональных языках, что ее было решено включить в Scala, однако для того чтобы она работала корректно, пришлось ввести одно необычное правило: любой метод с "забавным именем", заканчивающимся двоеточием, является правоассоциативным. Это означает, что выражение интерпретируется справа налево, т.е. список строится начиная с правого элемента Nil, который, к счастью, сам по себе является списком. Таким образом, :: является глобальным методом, а не методом-членом конкретного класса (в данном случае, String), поэтому с его помощью можно создавать списки из любых элементов. При использовании :: крайним правым элементом также должен быть список, иначе компилятор выдаст сообщение об ошибке.

Что значит "правоассоциативный"?

Для того чтобы лучше понять ситуацию с методом ::, следует помнить, что операторы (в частности, оператор сцепления) – это всего лишь методы с "забавными именами". При использовании обычного левоассоциативного синтаксиса крайний левый токен интерпретируется как объект, у которого будет вызван метод (токен справа). Поэтому обычно выражение 1 + 2 интерпретируется компилятором как 1.+(2) .

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

В Scala эту проблему удалось обойти, установив, что все методы с "забавными именами", заканчивающимися двоеточием (например, ::, ::: или нестандартные методы, скажем, foo:) должны быть правоассоциативными. Например, выражение a :: b :: c :: Nil преобразуется в Nil.::(c).::(b).::(a), т.е. интерпретируется именно так, как нужно – сначала вызывается метод List, а затем каждый последующий метод :: принимает на вход объект и возвращает опять экземпляр List, и так далее по цепочке.

Было бы неплохо, если бы была возможность задавать правило правоассоциативности для других конструкций, однако на момент написания этой статьи данное правило было жестко зашито внутрь Scala. На настоящее время двоеточие – это единственный символ, который может изменить ассоциативность метода.

Наиболее полно возможности списков в Scala раскрываются в сочетании с методом сопоставления с образцом. В Scala поддерживается не только проверка типов и значений элементов списка, но также и автоматическая привязка к переменным. Например, можно упростить код из листинга 10 путем использования сопоставления с образцом для проверки того, содержится в списке хотя бы один элемент или нет (листинг 11).

Листинг 11. Обработка списка методом сопоставления с образцом
  @Test def recurseWithPM =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case h :: t => count(t) + 1
        case Nil => 0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

Первое выражение case будет извлекать вершину списка и связывать с ней переменную h, в то время как остальная часть списка будет связана с переменной t. В данном примере переменная h далее никак обрабатываться не будет, поэтому было бы лучше выразить это явно путем замены h на специальный символ _. Переменная t будет рекурсивно передаваться в метод count, в точности как в предыдущем примере. Не забывайте, что в Scala каждое выражение имеет значение, поэтому в этом примере результатом сопоставления с образцом будет результат рекурсивного вызова count + 1, либо 0 при достижении конца списка.

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

Листинг 12. Поиск объекта "Amanda" методом сопоставления с образцом
  @Test def recurseWithPMAndSayHi =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    var foundAmanda = false
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case "Amanda" :: t =>
          System.out.println("Hey, Amanda!"); foundAmanda = true; count(t) + 1
        case h :: t =>
          count(t) + 1
        case Nil =>
          0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
    assertTrue(foundAmanda)
  }

Преимущества начинают отчетливо проявляться достаточно скоро, особенно при работе с регулярными выражениями или узлами XML. Кроме того, область применения сопоставления с образцом отнюдь не ограничивается списками, в частности, его можно использовать в приведенных выше примерах, иллюстрирующих работу с массивами. В листинге 13 показана реализация функции recurseWithPMAndSayHi из листинга 12, но на основе массива.

Листинг 13. Поиск элемента в массиве при помощи сопоставления с образцом
  @Test def recurseWithPMAndSayHi =
  {
    val myVIPList = Array("Ted", "Amanda", "Luke", "Don", "Martin")

    var foundAmanda = false
    
    myVIPList.foreach((s) =>
      s match
      {
        case "Amanda" =>
          System.out.println("Hey, Amanda!")
          foundAmanda = true
        case _ =>
          ; // Do nothing
      }
    )

    assertTrue(foundAmanda)
  }

В качестве упражнения вы можете попробовать реализовать рекурсивный вариант функции в листинге 13, которая выполняет подсчет элементов без использования изменяемой переменной var, определенной в области видимости recurseWithPMAndSayHi (вам может потребоваться несколько блоков сопоставления с образцом). Одно из возможных решений содержится в архиве с исходным кодом к статье, но будет лучше, если вы сначала попробуете найти его самостоятельно.


Заключение

Об этой серии

Тед Ньювард погружается в язык программирования Scala и берет вас с собой. В этой новой серии developerWorks вы узнаете, вокруг чего поднята такая шумиха и увидите некоторые лингвистические возможности Scala в действии. Код Scala и код Java будут показаны в сравнении, если это важно, но (как скоро выяснится) многие вещи в Scala не могут быть напрямую соотнесены с чем-либо таким, что вам знакомо из Java-программирования, но в этом и заключается основное очарование Scala! В конце концов, если Java-код способен это сделать, зачем же утруждать себя изучением Scala?

Широкий набор типов коллекций в Scala является прямым следствием его истории и возможностей как функционального языка программирования. Кортежи представляют собой удобный способ объединения нескольких элементов в один объект, а тип Option[T] позволяет легко различать наличие значения от его отсутствия. Кроме того, массивы являются объектами, обладающими стандартной для Java семантикой и некоторыми дополнительными возможностями. Наконец, Scala поддерживает списки – тип-коллекцию, который является стандартным для множества функциональных языков.

Однако будьте осторожны при работе с данными конструкциями, в частности, с кортежами. Часто возникает соблазн злоупотребления кортежами в ущерб традиционному объектному дизайну приложения. Если кортеж определенного вида, например, набор "имя, возраст, зарплата, список известных языков программирования", регулярно используется в разных частях программы, то его следует представлять в виде отдельного класса.

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


Загрузка

ОписаниеИмяРазмер
Исходный код примеров к статьеj-scala06278.zip200 KБ

Ресурсы

Научиться

  • Оригинал статьи: "The busy Java developer's guide to Scala: Collection types". (EN)
  • Прочитайте первую статью под названием "Путеводитель по Scala для Java-разработчиков: Функциональное программирование вместо объектно-ориентированного" (Тед Ньювард, developerWorks, январь 2008 г.), в которой приводится обзор языка Scala и, в частности, его функционального подхода к задачам параллельной обработки данных.
  • Прочитайте статью "Функциональное программирование на Java" (Абхиджит Белапуркар, developerWorks, июль 2004 г.), в которой обсуждаются преимущества и возможности применения функционального программирования с точки зрения разработчика Java. (EN)
  • Ознакомьтесь со статьей "Мертва как COBOL" (Тед Ньювард, developerWorks, май 2008 г.), в которой обсуждается вопрос о том, стоит ли переходить с Java на другие языки программирования. Тед Ньювард рассматривает аргументы как "за", так и "против" того, что Java является устаревшим языком. (EN)
  • Прослушайте трансляцию дискуссии Тед Ньюварда и Эндрю Гловера (Andrew Glover) на тему "Зачем нужен Scala? (JavaWorld, июнь 2008 г.). Вы узнаете мнение Теда о функциональном программировании вообще и месте Scala в мире Java. (EN)
  • Ознакомьтесь со статьей "Scala в примерах" (Мартин Одерски, декабрь 2007 г.), в которой приводится краткое введение в Scala, изобилующее примерами (формат PDF). (EN)
  • Прочитайте книгу Программирование на Scala (Мартин Одерски, Лекс Спун и Билл Веннерс; сигнальный экземпляр вышел в Artima в феврале 2008 г.) – первое подробное введение в Scala, написанное в соавторстве с Биллом Веннерсом. (EN)
  • Scaladocs: спецификация API стандартной библиотеки Scala. (EN)
  • Сотни статей по всем аспектам программирования на Java можно найти на сайте developerWorks, в разделе Технология Java.

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=390826
ArticleTitle=Путеводитель по Scala для Java-разработчиков: Коллекции
publish-date=05212009