Java.next: Языки Java.next

Использование языков Groovy, Scala и Clojure во все более многоязычном мире

Эта статья открывает новый цикл статей Нила форда на developerWorks, в которых проводится глубокое сравнение трех языков программирования нового поколения для платформы JVM: 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 приняли блестящее решение отделить язык от среды выполнения, что в конечном счете позволило работать на платформе Java более 200 языкам. Эта архитектура имеет решающее значение для долгосрочной жизнеспособности платформы, потому что жизнь языка программирования, как правило, коротка. С 2008 года ежегодный Саммит JVM-языков, организованный Oracle, предоставляет создателям альтернативных языков на платформе JVM возможность открыто сотрудничать с инженерами платформы Java.

Мы представляем читателю цикл статей Java.next. Он посвящен трем современным JVM-языкам — Groovy, Scala и Clojure, — которые предлагают интересное сочетание парадигм, вариантов дизайна и удобств. Я не буду здесь углубляться в описание каждого языка; его можно найти на соответствующих Web-сайтах (см. раздел Ресурсы). Но на Web-сайтах сообщества пользователей языков (основная цель которых — их популяризация) отсутствует объективная информации или примеры задач, для решения которых язык не подходит. Независимые сравнения, которые я провожу в этом цикле статей, призваны заполнить эти пробелы. Эта статья готовит почву для обзора языков Java.next и тех преимуществ, которые дает их освоение.

За пределами Java

Язык Java вырос из того, что Брюс Тейт в своей книге За пределами Java (см. раздел Ресурсы) называет идеальным штормом: совокупности таких факторов, как рост Web, вызванные различными причинами несоответствия между существующими Web-технологиями и рост интенсивности разработки многоуровневых приложений предприятиями. Тейт также утверждает, что этот идеальный шторм ― уникальное совпадение событий и что ни один другой язык уже никогда не вырастет подобным образом до той же относительной значимости.

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

Многоязычное программирование

Многоязычное программирование— термин, который я воскресил и заново популяризовал в своем блоге в 2006 году (см. раздел Ресурсы), — основано на понимании того, что ни один язык не подходит для решения всех задач. Некоторые языки имеют встроенные проекции или функции, которые лучше приспособлены для задач того или иного типа. Например, каким бы изощренным ни был инструментарий Swing, разработчики считают его неоправданно громоздким для написания пользовательских Swing-интерфейсов на платформе Java, потому что это влечет за собой необходимость объявления типов, появление неуклюжих анонимных внутренних классов поведения и другие неудобства. Swing-приложения становятся гораздо компактнее при использовании языка, который лучше подходит для построения UI, такого как Groovy с его средствами SwingBuilder (см. раздел Ресурсы).

Распространение языков, работающих на JVM, делает идею многоязычного программирования все более привлекательной, потому что это позволяет смешивать и сочетать различные языки, сохраняя один и тот же базовый байт-код и библиотеки. Например, SwingBuilder не заменяет Swing; он наслаивается поверх существующего API Swing. Конечно, разработчики давно смешивают языки и вне JVM — например, применяя для определенных целей SQL и JavaScript, — но в границах платформы JVM эта практика становится все более распространенной. Во многих проектах ThoughtWorks применяется несколько языков, и все инструменты, разработанные ThoughtWorks Studios, используют смешанные языки.

Даже если Java останется вашим основным языком программирования, освоение альтернативных языков позволит стратегически применять их. Java будет оставаться важной частью экосистемы JVM, но все больше как язык ассемблера для этой платформы — место, куда приходят исключительно по соображениям производительности или для удовлетворения особых требований.

Эволюция

В начале 1980-х, когда я учился в университете, мы использовали среду разработки под названием Pecan Pascal. Ее уникальной особенностью было то, что один и тот же код Pascal можно выполнять и на IBM PC, и на Apple II. Создатели Pecan достигли этого, используя нечто таинственное под названием «байт-код». Разработчики компилировали свой код Pascal в этот «байт код», который работал на «виртуальной машине», специально написанной для каждой платформы. Это было ужасно! Результирующий код работал болезненно медленно даже при выполнении простых назначений классов. Оборудование того времени просто не справлялось с этой задачей.

Спустя десятилетие после Pecan Pascal Sun выпустила Java с использованием той же архитектуры и с трудом, но преуспела в среде оборудования середины 1990-х. Она также добавила другие удобные для разработчика функции, такие как автоматическая сборка мусора. Проработав с такими языками, как C++, я больше не хочу программировать на языке, где нет сборки мусора. Лучше потратить время на более высокоуровневые абстракции, придумывая способы решения сложных бизнес-задач, чем на трудоемкие сантехнические работы типа управления памятью.

Одна из причин, по которым языки программирования обычно живут недолго, ― это быстрые темпы инноваций в области языков и платформ. По мере того как платформы становятся более мощными, они приобретают способность выполнять больше малопродуктивной работы. Например, функция мемоизации Groovy (добавленная в 2010 году) кэширует результаты вызовов функций. Вместо того чтобы писать код кэширования вручную, что чревато ошибками, достаточно вызвать метод memoize(), как показано в листинге 1.

Листинг 1. Мемоизация функции в Groovy
def static sum = { number ->
  factorsOf(number).inject(0, {i, j -> i + j})
}
def static sumOfFactors = sum.memoize()

В листинге 1 автоматически кэшируются результаты вызова метода sumOfFactors. Поведение кэширования можно настроить и с помощью альтернативных методов memoizeAtLeast() или memoizeAtMost(). Мемоизация есть и в Clojure, и ее легко реализовать в Scala. Из языков нового поколения (и некоторых Java-сред) расширенные функции, такие как мемоизация, постепенно проникают в язык Java. В следующей версии Java появятся более высокоуровневые функции, что значительно упростит реализацию мемоизации. Так что изучение языков Java нового поколения позволяет заглянуть в будущее самого языка Java.


Groovy, Scala, Clojure

Groovy ― это синтаксис Java двадцать первого века, эспрессо вместо обычного кофе. Цель разработки Groovy ― модернизировать синтаксис языка Java и исключить из него неудобства, поддерживая основные парадигмы. Поэтому Groovy «знает» о таких вещах, как JavaBeans, упрощая доступ к свойствам. Groovy быстрыми темпами приобретает все новые возможности, в том числе некоторые важные функциональные особенности, о которых я расскажу в последующих статьях. Groovy остается в первую очередь объектно-ориентированным, императивным языком. Два основных отличия от Java заключаются в том, что Groovy ― динамически, а не статически типизированный язык и что у него значительно более широке возможности метапрограммирования.

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

Будучи диалектом Lisp, Clojure представляет собой наиболее радикальный отход от других языков в плане синтаксиса. В высокой мере динамически типизированный язык (как и Groovy), он отражает категоричные дизайнерские решения. Хотя Clojure обеспечивает полное и глубокое взаимодействие с традиционным языком Java, он не пытается навести мосты со старой парадигмой. Например, Clojure откровенно функционален и поддерживает объектную ориентацию лишь для обеспечения взаимодействия. Тем не менее, он поддерживает все возможности, к которым привыкли приверженцы объектно-ориентированного программирования, такие как полиморфизм — но, скорее, в функциональной, чем в объектно-ориентированной манере. Clojure опирается на несколько основополагающих технических принципов, таких как программная транзакционная память, которые нарушают старые парадигмы ради новых возможностей.


Парадигмы

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

Статическая и динамическая типизация

Статическая типизация в языках программирования означает явное объявление типов, такое как декларация Java int x;. Термин динамическая типизация относится к языкам, которые не требуют объявления сведений о типе. Все рассматриваемые языки ― это строго типизированные языки, что означает, что код может установить тип после присваивания.

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

У Groovy есть одна особенность, которая на первый взгляд кажется преодолением пропасти между статической и динамической типизацией. Рассмотрим простую фабрику коллекций, показанную в листинге 2.

Листинг 2. Фабрика коллекций Groovy
class CollectionFactory {
  def List getCollection(description) {
    if (description == "Array-like")
      new ArrayList()
    else if (description == "Stack-like")
      new Stack()
  }
}

Класс, приведенный в листинге 2, действует как фабрика, возвращая любой из двух конструкторов интерфейса List ArrayList или Stack — в зависимости от переданного параметра description. Это как будто гарантирует Java-разработчикам, что все, что возвращается, соответствует этому соглашению. Однако два модульных теста, приведенных в листинге 3, выявляют осложнения.

Листинг 3. Тестирование типов коллекций в Groovy
@Test
void test_search() {
  List l = f.getCollection("Stack-like")
  assertTrue l instanceof java.util.Stack
  l.push("foo")
  assertThat l.size(), is(1)
  def r = l.search("foo")
}

@Test(expected=groovy.lang.MissingMethodException.class) 
void verify_that_typing_does_not_help() {
  List l = f.getCollection("Array-like")
  assertTrue l instanceof java.util.ArrayList
  l.add("foo")
  assertThat l.size(), is(1)
  def r = l.search("foo")
}

В первом тесте из листинга 3 я извлекаю Stack посредством фабрики, проверяю, что это действительно Stack, и выполняю Stack-подобные операции, такие как push(), size() и search(). Однако во втором тесте, чтобы он прошел, мне приходится защитить его от ожидаемого исключения MissingMethodException. Получив свою коллекцию Array-like в переменную, типизированную как List, я могу убедиться, что я действительно получил список. Однако при попытке вызвать метод search() случается исключение, потому что ArrayList не содержит метода search(). Таким образом, сделанная декларация не гарантирует правильность вызова метода на момент компиляции.

Это может показаться ошибкой, но это правильное поведение. Типы в Groovy обеспечивают только правильность оператора присваивания. Например, в листинге 3, если бы я возвратил что-то, что не реализует интерфейс List, произошло бы исключение времени выполнения (GroovyCastException). Это прочно удерживает Groovy в ряду существенно динамически типизированных языков вместе с Clojure.

Однако недавние изменения в языке сделали грань между статической и динамической типизацией в Groovy еще более размытой. В Groovy 2.0 появилась аннотация @TypeChecked, которая позволяет в каждом случае принимать отдельное решение по поводу строгости проверки типа на уровне класса или метода. Эта аннотация показана в листинге 4.

Листинг 4. Проверка типа посредством аннотации
@TypeChecked
@Test void type_checking() {
    def f = new CollectionFactory()
    List l = f.getCollection("Stack-like")
    l.add("foo")
    def r = l.pop()
    assertEquals r, "foo"
}

В листинге 4 я добавил аннотацию @TypeChecked, которая проверяет присваивание и последующий вызов метода. Например, код из листинга 5 больше не будет компилироваться.

Листинг 5. Проверка типа, предотвращающая неверный вызов метода
@TypeChecked
@Test void invalid_type() {
    def f = new CollectionFactory()
    Stack s = (Stack) f.getCollection("Stack-like")
    s.add("foo")
    def result = s.search("foo")
}

В листинге 5 нужно добавить приведение типа, возвращаемого фабрикой, что позволит вызвать метод search() из Stack. Это средство имеет ограничения: когда включена статическая типизация, многие динамические функции Groovy не работают. Однако этот пример иллюстрирует продолжающуюся эволюцию Groovy по преодолению пропасти между статической и динамической типизацией.

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

Императивные и функциональные языки

Другая важная ось сравнения языков программирования ― императивные и функциональные языки. Императивное программирование фокусируется на пошаговых инструкциях, во многих случаях подражая "древним" низкоуровневым аппаратным механизмам. Функциональное программирование больше сосредоточено на функциях как первоклассных конструкциях и пытается свести переходы состояний и изменчивость к минимуму.

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

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

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

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


Заключение

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

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

Ресурсы

Комментарии

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=932413
ArticleTitle=Java.next: Языки Java.next
publish-date=06032013