Содержание


Java.next

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

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Java.next

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Java.next

Следите за выходом новых статей этой серии.

В предыдущей статье я рассказал о новаторских способах, которыми языки 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 предлагают множество более мощных альтернатив.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

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