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

Управляющие конструкции Scala изнутри

Scala был создан специально для платформы Java™, поэтому его синтаксис сделан максимально удобным для Java-программистов. В то же время Scala привносит в JVM мощь, присущую функциональным языкам - но концепции функционального дизайна требуют некоторого времени для освоения. В этой очередной части из серии Путеводитель по Scala для Java-разработчиков Тед Ньюорд продолжает знакомить вас с нюансами в отличиях между двумя языками, начиная с управляющих конструкций типа if, while и for. Как вы узнаете, Scala наделяет эти конструкции мощью и развитостью, которых вы не найдете в соответствующих Java-эквивалентах.

Тед Ньюворд, директор, Neward & Associates

Тед Ньюворд (Ted Neward) является директором компании “Neward & Associates”. Он занимается консультированием, преподаванием и презентациями продуктов на основе Java, .NET, XML-сервисов и других платформ. В настоящее время он живет недалеко от Сиэттла, штат Вашингтон.



05.03.2009

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

Об этой серии

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

Например, рассмотрим принятый в Scala подход к реализации управляющих конструкций типа if, while и for. При том, что они могут выглядеть как привычные Java-конструкции, Scala наделяет их некоторыми уникальными характеристиками. Чтобы не подвергать вас риску исследования этих различий после того, как вы совершите кучу ошибок (и в результате напишете неработающий код), в статье за этот месяц предлагается изучить, чего следует ожидать при использовании управляющих структур в Scala.

Возвращаемся к Person.scala

В последней статье этой серии вы видели, как Scala позволяет определять POJO, задавая методы, имитирующие традиционные "getter-ы и setter-ы" необходимые для POJO-ориентированных сред. После публикации этой статьи я получил электронное письмо от Билла Веннерса (Bill Venners), соавтора готовящегося к выпуску канонического справочника по Scala Программирование на Scala (см. Ресурсы). Билл указал более простой способ достижения того же эффекта — использование аннотации scala.reflect.BeanProperty, скажем, так:

Листинг 1. Исправленный Person.scala
    class Person(fn:String, ln:String, a:Int)
    {
	@scala.reflect.BeanProperty
	var firstName = fn
	
	@scala.reflect.BeanProperty
	var lastName = ln
	
	@scala.reflect.BeanProperty
	var age = a

	override def toString =
	    "[Person firstName:" + firstName + " lastName:" + lastName +
		" age:" + age + " ]"
    }

Приведенный в листинге 1 подход (переработанный листинг 13 из моей предыдущей статьи) генерирует get/set-пару методов для указанной переменной var. Единственная оговорка — такие методы в действительности не существуют в Scala-коде и, стало быть, не могут быть вызваны другими частями кода. Как правило, это не является большой проблемой поскольку Scala будет использовать сгенерированные методы вместо самостоятельно сгенерированных полей; но вы можете быть неприятно удивлены, если не будете знать об этом заранее.

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

Иллюзия управления

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

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

От первого лица: Тед Ньюорд о Scala

Из этого подкаста JavaWorld вы узнаете о различиях между функциональными и объектно-ориентированными языками, а также о важных областях применения, в которых Java и другие объектно-ориентированные языки недостаточно эффективны.

Такая композиционная природа, проявляющаяся в вопросах надстраивания дополнительных возможностей над базовым набором языковых примитивов, имеет давнюю и богатую историю, берущую свое начало в далеких 1960-1970 годах в языках Smalltalk, Lisp и Scheme. В частности, языки наподобие Lisp и Scheme своей способностью определять высокоуровневые абстракции поверх низкоуровневых воспитали немало рьяных приверженцев этой идеи. Программисты брали высокоуровневые абстракции и использовали их для построения еще более высоких уровней. Когда вы слышите обсуждение такого процесса сегодня, это обычно касается предметно-специализированных языков (domain-specific languages, DSL— см. Ресурсы). На самом деле речь идет всего лишь о построении абстракций над абстракциями.

В языке Java ваша единственная альтернатива — использовать вызовы API; в Scala же вы можете привнести расширения в сам язык. При попытке расширить язык Java вы рискуете столкнуться с исключительными обстоятельствами, которые поставят под угрозу стабильность всего решения. Расширение Scala подразумевает всего лишь создание новой библиотеки.


Если бы да кабы

Мы начнем с традиционной конструкции if— несомненно, одной из простейших в обращении, не так ли? По крайней мере теоретически if просто проверяет условие, и если это условие является истинным, выполняет последующий блок кода.

Но эта простота обманчива! Традиционно язык Java предполагает для if лишь опциональное наличие оператора else, позволяя вам просто пропустить блок кода если исходное условие принимает значение "false". Однако в функциональном языке дела обстоят иначе. Следуя математической природе функционального языка, любая конструкция должна вычисляться в выражение, включая и оператор if. (Именно так работает известный Java-разработчикам тернарный оператор ?:).

В Scala ветка "false" (часть блока, соответствующая else) должна обязательно присутствовать и должна выдавать результат того же вида что и ветка if. Другими словами, независимо от ветвления кода всегда будет существовать результирующее значение. Рассмотрим, к примеру, следующий Java-код:

Листинг 2. Какой именно конфигурационный файл? (Java-версия)
// Это Java
String filename = "default.properties";
if (options.contains("configFile"))
  filename = (String)options.get("configFile");

Поскольку конструкция if в Scala сама по себе является выражением вы можете оформить приведенный выше код в, надо полагать, более корректный фрагмент, как показано в листинге 3:

Листинг 3. Какой именно конфигурационный файл? (Scala-версия)
// Это Scala
val filename =
  if (options.contains("configFile"))
    options.get("configFile")
  else
    "default.properties"

val против var

Вы можете подумать, что вам предстоит узнать нечто большее о различии val и var, но в действительности различие всего лишь таково — первое задает неизменяемое значение, а второе — изменяемую переменную. Как правило, функциональные языки, а в особенности те из них, что позиционируются как "чисто функциональные" (а, стало быть, не допускающие побочных эффектов типа изменяемости состояния сущностей), реализуют лишь val-концепцию; Scala же, взяв на себя обязательства угодить и функциональным и императивно-объектным программистам, предлагает оба варианта.

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

Впрочем, действительным преимуществом является то, что в Scala имеется возможность написать код для передачи результирующего значения в val вместо var. Будучи установленным, значение val не может быть изменено, что во многом похоже на поведение final-переменных в языке Java. Наиболее значимым побочным эффектом неизменяемости локальной переменной является упрощение параллелизма. Попытка проделать то же самое в Java-коде непременно приведет вас на грань того, что считается добротным, читабельным кодом, как показано в листинге 4:

Листинг 4. Какой именно конфигурационный файл? (Java с использованием тернарного оператора)
//Это Java
final String filename =
  options.contains("configFile") ?
    options.get("configFile") : "default.properties";

Объяснить происходящее при рецензировании такого кода может быть нелегко. Возможно, это и корректно, но многие Java-программисты взглянули бы искоса и спросили: "А зачем это?"


Пока вас не было ...

Теперь давайте взглянем на while и его младшего родственника do-while. По сути оба они делают одно и то же: проверяют условие и в случае его истинности продолжают исполнять подчиненный блок кода.

Как правило, функциональные языки избегают while-циклов, так как большинство того, что делает while, может быть реализовано с помощью рекурсии. Функциональные языки действительно любят рекурсию. Рассмотрим, например, реализацию алгоритма быстрой сортировки (quicksort), представленную в "Scala by Example" (см. Ресурсы) из комплекта поставки Scala.

Листинг 5. Быстрая сортировка (версия Java)
//Это Java
void sort(int[] xs) {
  sort(xs, 0, xs.length -1 );
}
void sort(int[] xs, int l, int r) {
  int pivot = xs[(l+r)/2];
  int a = l; int b = r;
  while (a <= b) {
    while (xs[a] < pivot) { a = a + 1; }
    while (xs[b] > pivot) { b = b – 1; }
    if (a <= b) {
      swap(xs, a, b);
      a = a + 1;
      b = b – 1;
    }
  }
  if (l < b) sort(xs, l, b);
  if (b < r) sort(xs, a, r);
}
void swap(int[] arr, int i, int j) {
  int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}

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

Достаточно сказать, что реализация quicksort в листинге 5 не совсем проста для восприятия и скрывает истинный смысл происходящего. Теперь рассмотрим прямой (в смысле наиболее близкий к вышеприведенной версии) эквивалент на Scala:

Листинг 6. Быстрая сортировка (версия Scala)
//Это Scala
def sort(xs: Array[Int]) {
  def swap(i: Int, j: Int) {
    val t = xs(i); xs(i) = xs(j); xs(j) = t
  }
  def sort1(l: Int, r: Int) {
    val pivot = xs((l + r) / 2)
    var i = l; var j = r
    while (i <= j) {
      while (xs(i) < pivot) i += 1
      while (xs(j) > pivot) j -= 1
      if (i <= j) {
	swap(i, j)
	i += 1
	j -= 1
      }
    }
    if (l < j) sort1(l, j)
    if (j < r) sort1(i, r)
  }
  sort1(0, xs.length 1)
}

Сам по себе код в листинге 6 очень похож на Java-версию. Отдельно стоит отметить его затянутость, громоздкость, неочевидность назначения (в частности, с точки зрения параллелизма), а потому — отсутствие преимуществ перед Java-версией.

По этой причине я его улучшу ...

Листинг 7. Быстрая сортировка (улучшенная версия Scala)
//Это Scala
def sort(xs: Array[Int]): Array[Int] =
  if (xs.length <= 1) xs
  else {
    val pivot = xs(xs.length / 2)
    Array.concat(
      sort(xs filter (pivot >)),
           xs filter (pivot ==),
      sort(xs filter (pivot <)))
  }

Очевидно, что код Scala в листинге 7 проще. Обратите внимание на использование рекурсии, позволяющей вообще обойтись без цикла while, а также вызовы функции filter на типе Array для отработки операций "больше-чем", "равно" и "меньше-чем" применительно к своим внутренним элементам. И, вдобавок, поскольку оператор if является выражением, возвращающим значение, возврат из sort() полностью образован единственным выражением, из которого и состоит определение sort().

Короче говоря, я выполнил рефакторинг цикла while и перенес изменяемое состояние из него непосредственно в параметры, передаваемые в различные вызовы sort(), о чем многие апологеты Scala могут сказать как о верном способе написания Scala-кода.

Возможно, также заслуживает внимания и то, что сам Scala не станет возражать, если вы примените while вместо рекурсии — вы, хвала небесам, не увидите предупреждения "Ты что, болван?" от компилятора. Scala также не станет оберегать вас от написания кода с изменяемым состоянием. Однако использование то ли while, то ли изменяемого состояния приносит в жертву ключевой аспект языка Scala, а именно поощрение написания хорошо параллелизуемого кода. Везде, где это возможно и удобно, "путь Scala" предполагает, что вы предпочтете рекурсию императивным блокам.

Написание своих собственных языковых конструкций

Мне бы хотелось отвлечься от обсуждения управляющих конструкций Scala, чтобы продемонстрировать то, что большинство Java-разработчиков найдут совершенно невероятным — создание ваших собственных языковых конструкций.

Особо дотошным "языковедам" будет интересно узнать, что цикл while, примитивная Scala-конструкция, может быть с тем же успехом представлен как предопределенная функция. Эта идея исследуется в документации Scala, где также приводится и определение этой гипотетической функции "While":

// Это Scala
def While (p: => Boolean) (s: => Unit) {
  if (p) { s ; While(p)(s) }
}

Указанные выше аргументы определяют выражение, возвращающее логическое значение и блок кода, который вообще ничего не возвращает (Unit), т.е. именно то, что и ожидает while.

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


Пробуем снова и снова

Конструкция try позволяет вам писать код наподобие такого:

Листинг 8. Если сначала что-то не вышло....
// Это Scala
val url =
  try {
    new URL(possibleURL)
  }
  catch {
    case ex: MalformedURLException =>
      new URL("www.tedneward.com")
  }

Код в Листинге 8 существенно отличается от примеров для if из листинга 2 или листинга 3. На самом деле было бы гораздо сложнее записать это в традиционном Java-коде, особенно если бы вы захотели сохранить полученное значение в неизменяемом месте (как я и поступил в заключительном примере в листинге 4). Еще одно очко на счет функциональной природы Scala!

Выражение case ex: из листинга 8 является частью другой конструкции Scala, выражения соответствия (match expression), которое применяется для операций сопоставления с шаблоном (pattern matching) в Scala. Мы рассмотрим сопоставление с шаблоном, являющееся общим признаком функциональных языков, немного позже; а пока думайте об этом как о концепции, соотносящейся со switch/case, как структуры struct в языке C соотносятся с классами.

Теперь давайте немного задумаемся об обработке исключений. Естественно, поддержка исключений в Scala интересна тем, что исключение представляется выражением, но одна из вещей, на которую рассчитывают разработчики — это стандартизованный способ обработки исключений, а не только возможность их перехвата. В AspectJ это делается через создание аспектов, которые оплетают собой различные части вашего кода, заданные через точки пересечения (pointcuts) и требуют осторожного написания, если вы желаете различного поведения в различных частях базового кода для различных типов исключений. SQLExceptions должно обрабатываться иначе, чем IOExceptions и так далее.

В Scala все это просто. Взгляните.

Листинг 9. Специализированное выражение для исключений
// Это Scala
object Application
{
  def generateException()
  {
    System.out.println("Генерирование исключения...");
    throw new Exception("Сгенерированное исключения");
  }

  def main(args : Array[String])
  {
    tryWithLogging  // Это не часть языка
    {
      generateException
    }
    System.out.println("Exiting main()");
  }

  def tryWithLogging (s: => Unit) {
    try {
      s
    }
    catch {
      case ex: Exception =>
        // куда бы вы хотели это записать?
	// Пока я выбрал консольное окно
	ex.printStackTrace()
    }
  }
}

Так же как и в случае с ранее рассмотренной конструкцией While, код для tryWithLogging является просто вызовом библиотечной функции (или, в данном случае, метода того же класса). В каждой ситуации могут быть задействованы различные варианты без необходимости написания сложного кода с пересечениями.

Вся прелесть такого подхода — в выгодном использовании возможности Scala заключать сложную логику в конструкции первого рода, на что до недавнего времени могло претендовать лишь аспектно-ориентированное сообщество. Конструкция первого рода из листинга 9 ловит исключения (как проверяемые так и непроверяемые) и обрабатывает их определенным способом. Дальнейшее развитие приведенной выше идеи ограничено только воображением. Вам лишь следует помнить, что Scala, как и многие функциональные языки, позволяет вам передавать блоки кода —читай функции — как параметры и использовать их затем желаемым образом.


Язык поколения "for"

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

Давайте начнем с рассмотрения того, как Scala управляется с простым последовательным итерированием по коллекции, что должно быть вам хорошо знакомо из Java-программирования:

Листинг 10. Всем и каждому
// Это Scala
object Application
{
  def main(args : Array[String])
  {
    for (i <- 1 to 10) // левосторонняя стрелочка означает в Scala "присваивание"
      System.out.println("Отсчет " + i)
  }
}

Здесь происходит в точности то, что ожидалось — 10-проходный цикл с выводом каждого значения. Однако будьте осторожны: выражение "от 1 до 10" вовсе не означает, что Scala имеет встроенную поддержку для целочисленного счета от 1 до 10. Технически происходящее гораздо менее очевидно: компилятор использует метод to, определенный на типе Int (все в Scala является объектом, помните?) для продуцирования объекта Range, который и содержит элементы для итерирования. Если вы перепишете этот код в подобие того, что "видит" компилятор Scala, то получите приблизительно следующее:

Листинг 11. Что видит компилятор
// Это Scala
object Application
{
  def main(args : Array[String])
  {
    for (i <- 1.to(10)) // левосторонняя стрелочка означает в Scala "присваивание"
      System.out.println("Отсчет " + i)
  }
}

Таким образом, for в Scala понимает числа ничуть не лучше, чем объекты любых других типов. Что воспринимается в действительности, так это scala.Iterable, задающий базовое поведение для итерирования по коллекции. Произвольная сущность, предоставляющая Iterable-средства (trait в терминологии Scala, но пока воспринимайте это как элемент интерфейса) может быть использована как точка приложения для оператора for. Списки List, массивы Array или даже ваши собственные пользовательские типы могут быть точно так же задействованы в выражениях с for.

Сближение Scala и английского языка

Вы, должно быть, отметили, насколько проще воспринимать Scala-версию цикла for в листинге 11. Это обязано тому факту, что объект Range является неявно инклюзивным с обеих сторон и ближе к синтаксису английского языка, чем Java. Наличие выражения с Range, сообщающего "от 1 до 10 делать это" избавляет от случайных ошибок выпадения из счета индексов с граничными значениями (off-by-one errors).

Особый "for"

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

Листинг 12. "Любит - не любит", отбор четных вариантов
// Это Scala
object Application
{
  def main(args : Array[String])
  {
    for (i <- 1 to 10; i % 2 == 0)
      System.out.println("Отсчет " + i)
  }
}

Заметили второе выражение в операторе for в листинге 12? Это фильтр — только элементы, преодолевшие фильтр (выражение разрешится в true) будут фактически "переданы" в тело цикла. В данном случае будут выведены только четные числа из диапазона 1...10.

Последующие блоки таких выражений в for также не обязательно должны быть "просто" фильтрами. Вы запросто можете незаметно дополнить набор выполняемых в цикле действий чем-нибудь незначительным (с точки зрения самого цикла). Например, вот таким кодом, отображающим текущее значение i до его изменения на следующем проходе цикла:

Листинг 13. Как я люблю тебя? Многословно, крошка
// Это Scala
object App
{
  def log(item : _) : Boolean =
  {
    System.out.println("Вычисление " + item)
    true
  }

  def main(args : Array[String]) =
  {
    for (val i <- 1 to 10; log(i); (i % 2) == 0)
      System.out.println("Отсчет " + i)
  }
}

При запуске каждый элемент из диапазона от 1 до 10 будет передан в log, где он затем будет автоматически "утвержден" через явное указание true в качестве возврата. Но следующая далее в for третья секция также делает свой взнос, отфильтровывая только те элементы, которые удовлетворяют критерию четности. Соответственно, опять-таки, в тело самого цикла будут переданы только четные числа.

Краткость - сестра "for"

То, что реализуется сложной последовательностью операторов в Java-коде, может быть ощутимо сокращено выражениями Scala. Например, вот один из способов просмотра директории при поиске всех файлов .scala с последующим отображением имени каждого вхождения:

Листинг 14. Поиск .scala
// Это Scala
object App
{
  def main(args : Array[String]) =
  {
    val filesHere = (new java.io.File(".")).listFiles
    for (
      file <- filesHere;
      if file.isFile;
      if file.getName.endsWith(".scala")
    ) System.out.println("Найден " + file)
  }
}

Такая разновидность фильтрующего for достаточно распространена (и точки с запятой в этом контексте также весьма надоедливы), чтобы заслужить разрешение опустить разделители. Действительно, Scala позволяет вам рассматривать операторы, расположенные между скобок в приведенном выше примере, непосредственно как блок кода:

Листинг 15. Поиск .scala (версия 2)
// Это Scala
object App
{
  def main(args : Array[String]) =
  {
    val filesHere = (new java.io.File(".")).listFiles
    for {
      file <- filesHere
      if file.isFile
      if file.getName.endsWith(".scala")
    } System.out.println("Найден " + file)
  }
}

Как Java-разработчик, вы можете счесть оригинальный синтаксис со скобками и точками с запятой более наглядным, а причудливый синтаксис без разделителей — усложняющим восприятие. К счастью, с точки зрения порождаемого кода оба синтаксических подхода идентичны.

FORмальные приемы

В операторных скобках for вы можете присвоить значение более чем одному элементу, как показано в листинге 16.

Листинг 16. Что в имени твоем?
// Это Scala
object App
{
  def main(args : Array[String]) =
  {
    // Обратите внимание на синтаксис инициализации массива: тип (Array[String])
    // выводится из типа входящих элементов
    val names = Array("Ted Neward", "Neal Ford", "Scott Davis",
      "Venkat Subramaniam", "David Geary")

    for {
      name <- names
      firstName = name.substring(0, name.indexOf(' '))
    } System.out.println("Найден " + firstName)
  }
}

Это так называемое присвоение в процессе выполнения операции (midstream assignment), и оно работает почти в точности так, как и можно предположить из приведенного кода: на каждой итерации цикла определяется новое значение firstName для хранения результата выделения подстроки вызовом substring, и затем вы можете использовать firstName в теле цикла.

Тут же возникает идея вложенной итерации, также расположенной внутри рассматриваемого выражения цикла:

Листинг 17. Быстрый просмотр на Scala
// Это Scala
object App
{
  def grep(pattern : String, dir : java.io.File) =
  {
    val filesHere = dir.listFiles
    for (
      file <- filesHere;
      if (file.getName.endsWith(".scala") || file.getName.endsWith(".java"));
      line <- scala.io.Source.fromFile(file).getLines;
      if line.trim.matches(pattern)
    ) println(line)
  }

  def main(args : Array[String]) =
  {
    val pattern = ".*object.*"
    
    grep pattern new java.io.File(".")
  }
}

В этом примере for, вызываемый внутри grep, использует две вложенных итерации — первая перебирает все файлы, обнаруженные в заданной директории (с привязкой каждого вхождения переменной file), а вторая просматривает все строки (привязка к line) в файле, отобранном на внешней итерации.

С помощью Scala-конструкции for вы можете проделать массу других вещей, но все предыдущие примеры следует рассматривать как убедительное доказательство моего утверждения: на самом деле for в Scala является конвейером, последовательно обрабатывающим коллекции элементов перед их передачей в тело цикла. Отдельные узлы этого конвейера также могут добавлять дополнительные элементы (генераторы), компоновать элементы, поступающие извне (фильтры) или выполнять нечто промежуточное (как в примере с записью в лог). В любом случае Scala делает значительный шаг вперед по сравнению с "расширенным циклом for", введенным в Java 5.


Найди мне соответствие

Последняя управляющая конструкция Scala, с которой вы сегодня познакомитесь — это match, предоставляющая большую часть возможностей Scala связанных с сопоставлениями по шаблону (pattern-matching). По сути, такие сопоставления описывают блоки кода вычисляемые на значение. Первое наилучшее соответствие приводит к выполнению упомянутого блока кода. Поэтому, например, в Scala вы можете встретить такое:

Листинг 18. Простое совпадение
// Это Scala
object App
{
  def main(args : Array[String]) =
  {
    for (arg <- args)
      arg match {
	case "Java" => println("Java приятен...")
	case "Scala" => println("Scala крут...")
	case "Ruby" => println("Ruby для зануд...")
	case _ => println("А ты что скажешь, VB-программист?")
      }
  }
}

При первом взгляде вы можете представить себе сопоставления по шаблону как String-совместимый "switch", дополненный символом подчеркивания, являющимся групповым подстановочным символом (wildcard) и в данном случае выступающем в роли варианта default, как и в обычных блоках switch. Однако такое восприятие в значительной степени преуменьшило бы уровень языка. Сопоставление по шаблону — еще одна возможность, обнаруживаемая во многих, если не в большинстве, функциональных языков, и она предоставляет существенную практическую мощь.

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

Теперь представьте, для примера, что у вас есть функция или метод объявляющий в качестве возвращаемого значения Object— хорошей аналогией будет результат возврата для метода java.lang.reflect.Method.invoke() из Java. Обычно, если мы говорим о Java, для вычисления результата вы сначала определяете его тип чтобы выполнить приведение; в Scala же для упрощения ситуации вы можете задействовать сопоставление по шаблону:

Листинг 19. Кто там?
// Это Scala
object App
{
  def main(args : Array[String]) =
  {
    // Тип Any в точности соответствует своему названию: является заместителем любого типа
    def describe(x: Any) = x match { 
      case 5 => "пять" 
      case true => "истина" 
      case "привет" => "здоров!" 
      case Nil => "пустой список" 
      case _ => "что-то еще" 
    }
    
    println describe(5)
    println describe("привет")
  }
}

Благодаря способности сопоставлений просто и компактно описывать правила соотнесения кода с различными значениями и типами, сопоставления по шаблону широко используются в синтаксических анализаторах (парсерах) и интерпретаторах, где текущая лексема из анализируемого потока выставляется против серии возможных выражений совпадения. Затем следующая лексема сопоставляется с другой серией и так далее. (Описанное отчасти отвечает на вопрос почему множество языковых парсеров, компиляторов и прочих ориентированных на код инструментов пишутся на функциональных языках вроде Haskell или ML).

Можно было бы продолжить разговор о сопоставлении с шаблонами, но это непосредственно привело бы нас к другой особенности Scala — ситуативным классам (case classes), о чем я предпочел бы поговорить в другой раз.


В заключение

Scala в различных своих проявлениях обманчиво схож с Java, и это как нигде очевидно в случае с оператором for. Функциональная природа синтаксических элементов уровня ядра предоставляет некоторые полезные свойства (такие, как уже упомянутые возможности присваивания), а также способность языка к расширению различными интересными путями без необходимости модифицировать сам компилятор javac. Это делает язык более пригодным как для определения внутренних DSL (тех DSL, которые определяются в рамках синтаксиса существующего языка), так и для использования программистами, склонными выстраивать абстракции из базового множества примитивов а-ля Lisp или Scheme.

О Scala можно говорить еще много, но наше время в этом месяце истекло. Не забудьте взять последнюю версию Scala (2.7.0-final на момент написания) и поиграть с прилагаемыми примерами для освоения языковых особенностей (см. Ресурсы). Помните — Scala снова возвращает функциональный подход в программирование!

Ресурсы

Научиться

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

Обсудить

Комментарии

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=374162
ArticleTitle=Путеводитель по Scala для Java-разработчиков: Не зацикливайтесь!
publish-date=03052009