Java.next: Языки Java.next: Общие черты Groovy, Scala и Clojure, часть 3

Переосмысление исключений, выражений и пустоты

Последняя из трех статей об общих чертах Clojure, Scala и Groovy посвящена тому, как эти языки справляются с проблемными областями языка Java™: исключениями, выражениями и значениями null. Каждый из языков Java.next устраняет недостатки языка Java благодаря уникальной реализации, которая подчеркивает характеристики этого языка.

Нил Форд, Архитектор приложений, ThoughtWorks

Нил Форд (Neal Ford) работает архитектором приложений в ThoughtWorks, революционной компании, предоставляющей профессиональные IT-услуги и помогающей талантливым людям по всему миру эффективнее использовать программное обеспечение. Он также является проектировщиком и разработчиком приложений, учебных материалов, журнальных статей, учебных курсов, видео/DVD-презентаций и автором книг "Разработка с Delphi: Объектно-ориентированный подход", "JBuilder 3 Unleashed" и "Искусство разработки Web-приложений на Java". Он специализируется на консультациях по построению широкомасштабных корпоративных приложений. Он также является общепризнанным докладчиком и выступал на многочисленных конференциях разработчиков по всему миру. С ним можно связаться по адресу: nford@thoughtworks.com.



25.07.2013

Об этом цикле статей

Наследником Java будет не язык, а платформа. На платформе JVM работает более 200 языков программирования, каждый из которых несет в себе новые интересные возможности, выходящие за рамки языка Java. Этот цикл статей посвящен исследованию трех языков нового поколения для платформы JVM — Groovy, Scala и Clojure, — а также сравнению и противопоставлению новых возможностей и парадигм. Он позволяет Java-разработчикам заглянуть в свое собственное ближайшее будущее — и помогает им принимать обоснованные решения о том, стоит ли тратить время на освоение нового языка.

В предыдущей статье я рассказал о новаторских способах, которыми языки Java.next устраняют недостатки языка Java, связанные с трудоемкостью и сложностью программирования. В этом выпуске я покажу, как эти языки справляются с некоторыми вещами, которые вызывают трудности при программировании на Java: исключениями, операторами/выражениями и крайними проявлениями значения null.

Выражения

Язык Java унаследовал от языка С различие между программными операторами и выражениями. Примерами Java-операторов служат строки кода, содержащие выражения if или while, а также те, в которых используется выражение void для объявления методов, не возвращающих никаких значений. Выражения, например, 1 + 2, вычисляют значение.

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

if и ?: в Groovy

Эволюцию в направлении выражений в языках Java.next отражает раскол между традиционным императивом (Groovy) и функциональными языками (Clojure и Scala). В Groovy еще есть операторы, основанные на синтаксисе Java, но добавляется больше выражений. Scala и Clojure используют только выражения.

Наличие операторов и выражений усложняет синтаксис языка. Рассмотрим оператор if, унаследованный Groovy от Java. У него есть две версии, которые сравниваются в листинге 1: оператор if для принятия решений и загадочный тернарный оператор ?:.

Листинг 1. Два варианта if в Groovy
def x = 5
def y = 0
if (x % 2 == 0)
  y = x * 2
else
  y = x - 1
println y   // 4

y = x % 2 == 0 ? (x *= 2) : (x -= 1)
println y   // 4

В операторе if из листинга 1 необходимость задавать значение x является побочным эффектом, потому что у оператора if нет возвращаемого значения. Чтобы принять решение и сделать назначение одновременно, необходимо использовать тройнное назначение, как во втором примере листинга 1.

Выражения if в Scala

Scala устраняет необходимость в тернарном операторе, позволяя в обоих случаях применять выражение if. Его можно использовать так же, как оператор if в Java-коде (игнорируя возвращаемое значение) или для присваивания, как показано в листинге 2.

Листинг 2. Выражения if в Scala
val x = 5
val y = if (x % 2 == 0) 
          x * 2
	else
	  x - 1
println(y)

Для методов Scala, как и в двух других языка Java.next, не требуется явного выражения return. При его отсутствии в последней строке метода содержится возвращаемое значение, что подчеркивает ориентированный на выражения характер методов в этих языках.

Как и при задании значений в Java и Groovy, каждый ответ можно инкапсулировать в блок кода, как показано в листинге 3, и включить туда любые желаемые побочные эффекты.

Листинг 3. Выражения if с побочными эффектами в Scala
val z = if (x % 2 == 0) {
              println("divisible by 2")
	      x * 2
	    } else {
              println("not divisible by 2; odd")
	      x - 1
	    }
println(z)

В листинге 3 я распечатал сообщение о состоянии для каждого случая с возвратом вычисленных значений. Важен порядок строк кода в блоке: в последней строке блока содержится возвращаемое значение для данного условия. Так что при смешивании оценки с побочными эффектами на основе выражения if требуется особое внимание.

Выражения и побочные эффекты в Clojure

Clojure также работает только с выражениями, но в отделении побочных эффектов от оценки идет еще дальше. Clojure-версия двух предыдущих примеров, приведенная в блоке let в листинге 4, позволяет определить локальные переменные.

Листинг 4. Пример выражения if на языке Clojure
(let [x 5
      y (if (= 0 (rem x 2)) (* x 2) (- x 1))]
  (println y))

В листинге 4 я присваиваю x значение 5, а затем создаю выражение с if для вычисления двух условий: (rem x2) вызывает функцию remainder, аналогично оператору % Java, и сравнивает результат с нулем, проверяя на нулевой остаток при делении на 2. В выражении Clojure if первый аргумент ― условие, второй ― ветвь true, а третий ― факультативная ветвь else. Результат выражения if присваивается переменной y, которая затем распечатывается.

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

Листинг 5. Явные побочные эффекты в Clojure
(let [x 5
      a (if (= 0 (rem x 2))
          (do
            (println "divisible by 2")
            (* x 2))
          (do
            (println "not divisible by 2; odd")
            (- x 1)))]
  (println a))

В листинге 5 переменной a присваивается возвращаемое значение выражения if. Для каждого из условий создается оболочка (do ...), что позволяет использовать произвольное количество операторов. Последняя строка блока содержит результат блока (do...), аналогично примеру для Scala, приведенному в листинге 3. Целевое возвращаемое значение вычисляется последним. Такое использование блоков (do...) настолько распространено, что многие конструкции в Clojure (например,(let [])) уже включают в себя неявные блоки (do ...), что во многих случаях устраняет потребность в них.

Разница в обработке выражений в Java/Groovy и Scala/Clojure свидетельствует об общей тенденции в языках программирования, направленной на устранение ненужной дихотомии операторы/выражения.


Исключения

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

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

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

Листинг 6. Исключения как возвращаемые значения
val quarter = 
  if (n % 4 == 0)
    n / 4
  else
    throw new RuntimeException("n must be quarterable")

В листинге 6 либо переменной присваивается четверть значения n, либо выдается исключение. В последнем случае возвращаемое значение игнорируется, потому что исключение распространяется до вычисления результата. Такое присваивание может показаться странным, учитывая, что Scala ― язык, основанный на типах. Тип исключения Scala ― не числовой тип, и разработчики не привыкли иметь дело с возвращаемым значением выражения throw. В Scala эта проблема гениально решена с помощью специального типа Nothing, используемого в качестве возвращаемого типа выражения throw. Any находится в верхней части иерархии наследования Scala (как Object в Java), а значит, его распространяют все классы. И наоборот, Nothing находится внизу, что автоматически делает его подклассом всех других классов. Таким образом, код из листинга 6 легитимен для компиляции: либо возвращается число, либо выдается исключение до установки возвращаемого значения. Компилятор не сообщает об ошибке, потому что Nothing является подклассом Int.

Во-вторых, блок finally интересно ведет себя в мире выражений. Блок finally Scala работает как другие функции, но с его возвращаемыми значениями связаны некоторые нюансы. Рассмотрим в качестве примера листинг 7.

Листинг 7. Возвращаемое значение finally в Scala
def testReturn(): Int = 
  try {                               
    return 1                          
  } finally {                         
    return -1                         
  }

В листинге 7 общее возвращаемое значение равно -1. Возвращаемое значение блока finally «переопределяет» то, которое находится в теле оператора try. Этот неожиданный результат имеет место только тогда, когда блок finally содержит явный оператор return; неявный результат игнорируется, как показано в листинге 8.

Листинг 8. Неявный результат Scala
def testImplicitReturn(): Int = 
  try {
    1 
  } finally {
   -1
  }

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

Clojure также целиком основан на выражениях. Возвращаемое значение (try ...) ― всегда одно из двух:

  • либо последняя строка блока try, если нет исключения;
  • либо последняя строка блока catch, в котором есть исключение.

Синтаксис исключений в Clojure показан в листинге 9.

Листинг 9. Блок (try...catch...finally) Clojure
(try  
  (do-work)
  (do-more-work)
  (catch SomeException e  
    (println "Caught" (.getMessage e)) "exception message")
  (finally  
    (do-clean-up)))

В листинге 9 возвращаемое значение для успешного пути берется из (do-more-work).

Языки Java.next берут лучшее из механизма исключений Java и отказываются от громоздких деталей. И несмотря на некоторые различия в реализации, им удается ввести исключения в мир языка, основанного на выражениях.


Пустота

В своей легендарной презентации на конференции QCon-2009 в Лондоне Тони Хоара назвал свое изобретение концепции «null» для ALGOL W — экспериментального объектно-ориентированного языка, который он представил в 1965 году, — «ошибкой на миллиард долларов» из-за всех тех проблем, к которым оно привело в соответствующих языках программирования. Сам язык Java страдает от проблем, связанных с граничными случаями использования значения null, которые решены в языках Java.next.

Например, общая идиома в Java-программирования защищает от исключения NullPointerException перед попыткой вызова метода:

if (obj != null) {
    obj.someMethod();
}

Groovy инкапсулирует этот шаблон в оператор "безопасного плавания" ?.. Он автоматически выполняет проверку на null с левой стороны и пытается вызвать метод только в том случае, если это не null; иначе он возвращает null.

obj?.someMethod();
def streetName = user?.address?.street

Вызовы оператора "безопасного плавания" могут быть вложенными.

Родственный оператор Элвиса Groovy, ?:, при наличии значений по умолчанию сокращает тернарный оператор Java. Например, следующие строки кода эквивалентны:

def name = user.name ? user.name : "Unknown" //традиционное использование 
    //тернарного оператора


def name = user.name ?: "Unknown"  // более компактный оператор Элвиса

Когда левая сторона уже имеет значение (как правило, по умолчанию), оператор Элвиса сохраняет его, в противном случае он присваивает новое значение. Оператор Элвиса — это более короткая версия тернарного оператора, ориентированная на выражения.

Язык Scala расширил концепцию null и сделал ее классом (scala.Null), который наряду с родственным классом scala.Nothing. null и Nothing находится в самом низу иерархии классов Scala. null - это подкласс каждого reference-класса, а Nothing ― подкласс любого другого типа.

Когда нужно указать на отсутствие значения, Scala предлагает альтернативу как null, так и исключениям. Многие операции Scala с коллекциями (такие как операция get с коллекцией Map) возвращают экземпляр параметра Option, который содержит любую из двух частей (но не обе): Some или None. В листинге 10 демонстрируется пример взаимодействия REPL.

Листинг 10. Возвращаемое значение Option в Scala
scala> val designers=Map("Java" -> "Gosling", "c" -> "K&R", "Pascal" -> "Wirth")
designers: scala.collection.immutable.Map[java.lang.String,java.lang.String] = 
	   Map(Java -> Gosling, c -> K&R, Pascal -> Wirth)

scala> designers get "c"
res0: Option[java.lang.String] = Some(K&R)

scala> designers get "Scala"
res1: Option[java.lang.String] = None

Обратите внимание на то, что результатом успешной операции get является Option[java.lang.String] = Some(value), тогда как безуспешный поиск дает None. Механизм распаковки значений из коллекций использует поиск по шаблону, который сам представляет собой выражение, что позволяет выполнять обращение и распаковку одним лаконичным выражением:

println(designers get "Pascal" match { case Some(s) => s; case None => "?"})

Option позволяет лучше выразить пустоту, чем просто null, особенно с учетом синтаксической поддержки его использования.


Заключение

В этом выпуске мы рассмотрели три проблемных области языка Java: выражения, исключения и значение null. Все языки Java.next устраняют болевые точки Java, но каждый по-своему. Присутствие выражений изменяет идиомы и варианты, казалось бы, не связанных концепций, таких как исключения — это дополнительная иллюстрация того, что особенности языка тесно связаны друг с другом.

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

Ресурсы

Комментарии

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, Open source
ArticleID=938665
ArticleTitle=Java.next: Языки Java.next: Общие черты Groovy, Scala и Clojure, часть 3
publish-date=07252013