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

Как языки Java.next борются со стереотипностью и сложностью

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

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

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



03.06.2013

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

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

Язык программирования Java появился в иных условиях, чем те, в которых программисты работают сегодня. В частности, существование в языке Java примитивных типов вызвано ограничениями в части производительности и памяти, характерными для аппаратуры середины 1990-х годов. С тех пор язык Java эволюционировал, но языки Java.next — Groovy, Scala и Clojure — пошли еще дальше в направлении устранения всевозможных несуразностей и трений.

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

Конец примитивов

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

Groovy полностью скрывает примитивные типы от программиста. Например, int всегда означает целое число, и Groovy автоматически управляет преобразованием числовых типов во избежание ошибок переполнения при работе с числами. Рассмотрим пример взаимодействия с Groovy-оболочкой, приведенный в листинге 1.

Листинг 1. Автоматическая обработка примитивов в Groovy
groovy:000> 1.class
===> class java.lang.Integer
groovy:000> 1e12.class
===> class java.math.BigDecimal

В листинге 1 Groovy-оболочка демонстрирует, что даже константы представлены базовыми классами. Поскольку все числа (и другие якобы примитивы) на самом деле суть классы, можно использовать приемы метапрограммирования. В число этих приемов входит добавление к числам методов — часто используемое для создания специализированных языков (domain-specific languages - DSL), допускающих такие выражения, как 3.cm. Я подробнее расскажу об этой возможности в следующей статье, посвященной расширяемости.

Clojure, как и Groovy, автоматически скрывает разницу между примитивами и оболочками, позволяя вызывать методы для всех типов и автоматически выполняя преобразования типов в целях обеспечения емкости. В Clojure скрыто огромное количество оптимизаций, которые хорошо отражены в документации языка (см. раздел Ресурсы). Во многих случаях можно делать намеки на тип, чтобы компилятор генерировал код быстрее. Например, в определение метода (defn sum[x] ... ) можно добавить намек на тип, такой как (defn sum[^float x] ... ), что приведет к созданию более эффективного кода критически важных разделов.

Scala также скрывает различия, как правило, используя примитивы ниже уровня критичных ко времени выполнения частей кода. Еще он позволяет вызывать методы для констант, например, 2. toString. Благодаря способности смешивать и сочетать примитивы и классы-оболочки, такие как Integer, Scala прозрачнее, чем автобоксинг Java. Например, оператор == в Scala, в отличие от Java-версии того же оператора, правильно работает (сравнивая значения, а не ссылки) как с примитивами, так и со ссылками на объекты. Еще в Scala есть метод eq (с симметричным методом ne), который всегда определяет равенство (или неравенство) для соответствующего типа ссылок. В основном Scala грамотно управляет своим поведением по умолчанию. В языке Java == сравнивает ссылки, чего почти никогда не требуется, тогда как для сравнения значений применяется менее интуитивно понятный метод equals(). В Scala == делает то, что нужно (сравнивает значения), независимо от реализации, и предоставляет метод для менее востребованной проверки равенства ссылок.

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


Упрощение поведения по умолчанию

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

Классы и case-классы Scala

Scala упрощает определения классов, автоматически создавая методы-аксессоры, мутаторы и конструкторы. Рассмотрим Java-класс, приведённый в листинге 2.

Листинг 2. Простой класс Java Person
class Person {
    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return name + " is " + age + " years old.";
    }
}

Единственный нестереотипный код в листинге 2 ― это переопределенный метод toString(). Конструктор же и все методы сгенерированы средой IDE. Важна не столько способность быстро создавать код, сколько возможность легко понять его позже. Лишний синтаксис добавляет объем кода, который нужно использовать, чтобы сделать понятным его смысл.

Класс Person Scala

В Scala эквивалентный класс создается с помощью потрясающе простого определения из трех строк, приведенного в листинге 3.

Листинг 3. Эквивалентный класс в Scala
class Person(val name: String, var age: Int) {
  override def toString = name + " is " + age + " years old."
}

Класс Person из листинга 3 сводится к изменяемому свойству age, неизменяемому свойству name и конструктору с двумя параметрами, плюс мой переопределенный метод toString(). Все, что есть уникального в этом классе, легко увидеть, потому что интересные части не тонут в синтаксисе.

Дизайн Scala подчеркивает возможность создания кода с минимальным избыточным синтаксисом и делает значительную часть синтаксиса необязательной. Простой класс, приведенный в листинге 4, ― это многословный класс, который переводит строку в верхний регистр.

Листинг 4. Многословный класс
class UpperVerbose {
  def upper(strings: String*) : Seq[String] = {
    strings.map((s:String) => s.toUpperCase())
  }
}

Большая часть кода в листинге 4 необязательна. В листинге 5 показан тот же код, но теперь это объект, а не класс.

Листинг 5. Упрощенный объект преобразования в верхний регистр
object Up {
  def upper(strings: String*) = strings.map(_.toUpperCase())
}

В Scala-эквивалентах статических методов Java вместо класса создается объект— встроенный Scala-эквивалент singleton-экземпляра. В листинге 5 возвращаемый методом тип из листинга 4, скобки, разграничивающие тело однострочного метода, и необязательный параметр s исчезают. Этот «сворачиваемый синтаксис» ― одновременно и благо, и проклятие Scala. Он позволяет писать необычайно идиоматичный код, но это может затруднить его понимание для непосвященных.

Case-классы

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

  • Можно создать основанный на имени класса метод-фабрику. Например, новый экземпляр ― без возни с ключевым словом new: val bob = Person("Bob", 42).
  • Все аргументы в списке параметров класса автоматически становятся val, что означает, что они обслуживаются как неизменяемые внутренние поля.
  • Компилятор генерирует для вашего класса разумные методы по умолчанию equals(), hashCode() и toString().
  • Он добавляет к классу метод copy(), так что можно вносить мутирующие изменения, получая новую копию.

Языки Java.next не только исправляют синтаксические изъяны, но и способствуют более глубокому пониманию того, как работает современное программное обеспечение, приспосабливая для него свои средства.

Автогенерируемые свойства Groovy

Среди языков Java.next Groovy ближе всего придерживается синтаксиса Java, обеспечивая для общих случаев автоматическую генерацию кода. Рассмотрим простой класс Person в Groovy (листинг 6).

Листинг 6. Класс Person в Groovy
class Person {
  private name
  def age

  def getName() {
    name
  }

  @Override
  String toString() {
    "${name} is ${age} years old."
  }
}

def bob = new Person(name: "Bob", age:42)

println(bob.name)

В коде Groovy, приведенном в листинге 6, определение поля def приводит к созданию как аксессора, так и мутатора. Если вы предпочитаете только то или другое, то можно определить его самостоятельно, что я и делаю для свойства name. Несмотря на то что метод называется getName(), я могу обращаться к нему посредством более интуитивно понятного синтаксиса bob.name.

Если нужно, чтобы Groovy автоматически сгенерировал пару методов equals() и hashCode(), добавьте к классу аннотацию @EqualsAndHashCode. Эта аннотация использует функцию Groovy Abstract Syntax Tree (AST) Transformations для создания методов, основанных на ваших свойствах (см. раздел ресурсы). По умолчанию эта аннотация учитывает только свойства (не поля); если добавить модификатор includeFields = true, то она будет учитывать и поля тоже.

Map-подобные записи Clojure

В Clojure можно создать тот же класс Person, как и в других языках, но он не будет идиоматическим. Для хранения такой информации языки типа Clojure традиционно опираются на структуры данных map (пара имя-значение) и предлагают функции, работающие с этой структурой. Хотя структурированные данные можно по-прежнему моделировать в map-структурах, сегодня более распространенным методом является использование записей. Запись – это более формальная инкапсуляция языком Clojure имени типа и свойств (часто вложенных), несущих одно и то же смысловое значение для данного экземпляра. (Записи в Clojure аналогичны структурам struct в C-подобных языках.)

Например, рассмотрим следующее определение для Person:

(def mario {:fname "Mario"
            :age "18"})

С учетом этой структуры можно обращаться к age посредством выражения (get mario :age). Простой доступ ― это обычная операция для map-структур. В Clojure можно использовать тот синтаксически благоприятный факт, что ключи выступают в качестве функций доступа к своим отображениям, и использовать более короткое выражение (:age mario). Clojure рассчитан на операции с map-структурами, поэтому в нем много "синтаксического сахара" для их облегчения.

В Clojure есть также синтаксический сахар для доступа к вложенным элементам map-структур, как показано в листинге 7.

Листинг 7. Короткий способ доступа в Clojure
(def hal {:fname "hal"
          :age "17"
          :address {:street "Enfield Tennis Academy"
                    :city "Boston"
                    :state "MA"}})

(println (:fname hal))
(println (:city (:address hal)))
(println (-> hal :address :city))

В листинге 7 я определяю структуру вложенных данных с именем hal. Доступ к внешним элементам осуществляется, как и ожидалось ((:fname hal)). Как показывает предпоследняя строка листинга 7, синтаксис Lisp выполняет оценку «наизнанку». Сначала нужно получить запись address от hal, а затем обращаться к полю city. Так как оценка «наизнанку» общепринята, в Clojure есть специальный оператор (оператор направления-> ), который инвертирует выражения, чтобы сделать их более удобочитаемыми: (-> hal :address :city).

Эквивалентную структуру можно создать и с записями, как показано в листинге 8.

Листинг 8. Создание структуры с записями
(defrecord Person [fname lname address])
(defrecord Address [street city state])
(def don (Person. "Don" "Gately" 
           (Address. "Ennet House" "Boston", "MA")))

(println (:fname don))
(println (-> don :address :city))

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

В Clojure 1.2 добавлен синтаксический сахар, связанный с общей операцией определения записи посредством двух функций-фабрик:

  • ->TypeName, которая принимает параметры положения для полей, и
  • map->TypeName, которая сопоставляет ключевые слова со значениями полей.

Используя идиоматическую функцию, код из листинга 8 можно преобразовать в версию, приведенную в листинге 9.

Листинг 9. Услащенный синтаксический сахар Clojure
(def don (->Person "Don" "Gately"В 
В  (->Address "Ennet House" "Boston", "MA"))) В 

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

Clojure использует записи и протоколы вместе с кодом структуры. Это соотношение я рассмотрю в следующей статье.


Заключение

Все три языка Java.next предоставляют синтаксические удобства по сравнению с языком Java. Groovy и Scala облегчают создание классов и общих случаев, тогда как Clojure делает беспрепятственным взаимодействие map-структур, записей и классов. Общая тема, пронизывающая все языки 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=932417
ArticleTitle=Java.next: Общие черты Groovy, Scala и Clojure, часть 2
publish-date=06032013