Путеводитель по Scala для Java-разработчиков: Функциональное программирование вместо объектно-ориентированного (исходники)

Тед Ньювард

Вы никогда не забудете свою первую любовь.

В моем случае ее звали Табинда (Бинди) Хан. Это были безмятежные годы моей юности, седьмой класс, если быть точным, а она была красива, смышлена и, что самое замечательное, она хохотала над моими неуклюжими подростковыми шуточками. Мы "запали" друг на друга (как это тогда называлось), со взлетами и падениями в отношениях, длившихся преимущественно в 7 и 8 классах. Но к 9 классу мы расстались, это было вежливым способом сказать, что ей надоело на протяжении двух лет выслушивать однообразные и нескладные юношеские хохмы. Я никогда ее не забуду (не в последнюю очередь потому, что мы столкнулись друг с другом снова на 10-летней встрече одноклассников), но что более важно - я никогда не расстанусь с этими заветными (если несколько преувеличить) воспоминаниями.

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

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

Я буду любить тебя всегда ...

В течение последних пяти лет обозначилась нарастающая волна недовольства языком Java. В то время, как кое-кто может указать на развитие Ruby on Rails как на основной фактор в этом смысле, я приведу доводы что RoR (в нотации, знакомой поклонникам Ruby) является следствием, а не причиной. Или же, выражаясь точнее, докажу, что факт восприятия Java-разработчиками Ruby следует из глубинной, более скрытой причины.

Попросту говоря, в программировании на Java проступает возраст.

Или, что будет точнее, возраст проступает у языка Java.

И действительно: когда язык Java впервые появился на свет, Клинтон (в свое первое президентство) восседал в офисе, а Интернетом регулярно пользовались лишь настоящие энтузиасты, главным образом потому, что соединение по телефонной линии было единственным доступным способом в домашних условиях. Блоги (сетевые дневники) еще не были изобретены, и все верили, что наследование является фундаментальным подходом в повторном использовании. Мы также верили, что объекты являются наилучшим способом моделирования мира и что Закон Мура будет всегда господствовать экспоненциально.

На самом деле - именно законом Мура были в крайней степени озабочены многие в индустрии. С 2002/2003 доминирующей тенденцией в микропроцессорных технологиях было создание процессорных модулей с множеством "ядер": в сущности, множества процессоров на одном чипе. Это позволяет обойти Закон Мура, который говорит, что скорость процессоров удваивается каждые 18 месяцев. Ситуация с многопоточными средами, исполняемыми на двух процессорах одновременно, вместо того, чтобы выполнять стандартный круговой цикл на едином процессоре, означает, что код должен быть непробиваемой глыбой с точки зрения потокобезопаности, коль скоро такой код претендует на существование.

В академическом сообществе предпринималось множество исследований касательно этой специфической проблемы, приведших к изобилию новоиспеченных языков. Существенным недостатком выступал тот факт, что большинство из этих языков надстраивались над своей собственной виртуальной машиной или интерпретатором, означая таким образом (как делает и Ruby) переход на новую платформу. Кризис параллелизма - настоящая головная боль и некоторые из новых языков предлагают мощные решения, но слишком много корпораций и предприятий помнят миграцию с C++ на платформу Java каких-то 10 лет тому назад. Перемещение на новую платформу - это риск, который многие компании даже не собираются рассматривать всерьез. Многие, в действительности, по-прежнему зализывают раны от последнего перехода на Java-платформу.

Знакомимся со Scala.

SCAlable LAnguage - Масштабируемый язык

 
Почему Scala?

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

  • Создавать внутренние DSL - языки программирования, ориентированные на моделирование (DSL - Digital Simulation Language) типа Ruby, благодаря гибкой реализации идентификаторов в Scala.
  • Создавать в наивысшей степени масштабируемые, параллельные обработчики данных, благодаря тому, что Scala изначально стоит на позициях неизменяемости состояния .
  • Сократить размер эквивалентного Java-кода в половину или на две трети, из-за обилия в Scala синтаксических приемов, таких как замыкания и неявные определения.
  • Использовать преимущества параллельных аппаратных архитектур (таких как многоядерные процессоры), т.к. Scala предрасполагает к функциональному дизайну.
  • Контролировать большие объемы кода, из-за упрощения в Scala правил жесткого типизирования, выставляющих, по существу, одно требование - "все является объектом."

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

Scala - функционально-объектный гибридный язык с несколькими сильными сторонами, подогревающими интерес к нему:

  • Во-первых, Scala компилируется в байт-код Java, подразумевая его запуск на JVM. В дополнение к вашей возможности продолжать использовать все преимущества Java как развитой экосистемы с открытым кодом, Scala может быть интегрирован в существующее информационное пространство (среду) с нулевыми усилиями на миграцию.
  • Во-вторых, Scala опирается на функциональные принципы Haskell и ML, не отказываясь от тяжкого бремени привычных объектно-ориентированных концепций, столь полюбившихся Java-программистам. В результате Scala может смешивать лучшее из двух миров в единое целое, что предоставляет значительный выигрыш без жертвования простотой, на которую мы привыкли рассчитывать.
  • И в заключение, Scala был разработан Мартином Одерски, возможно, более известным в Java-сообществе благодаря языкам Pizza и GJ, последний из которых стал рабочим прототипом универсальных типов ( generics ) в Java 5. Раз так, Scala несет ощущение "серьезности"; этот язык не создавался по капризу и он не будет брошен на произвол.

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

Загрузка и установка Scala

Вы можете загрузить комплект поставки Scala с домашней страницы Scala. Текущим на момент написания статьи релизом является 2.6.1-final. Он доступен в виде Java-инсталлятора, пакетов RPM и Debian, архивов gzip/bz2/zip, которые достаточно просто распаковать в целевую директорию, а также в виде исходного tarball, позволяющего выполнить сборку с нуля. (Версия 2.5.0-1 доступна для пользователей Debian с Web-сайта Debian в виде готового к употреблению инсталляционного модуля. Однако версия 2.6 имеет некоторые незначительные отличия, поэтому рекомендуется загрузка и установка напрямую с сайта Scala.)

Установите Scala в каталог по выбору - я пишу это, находясь в среде Windows®, поэтому у меня это будет каталог C:/Prg/scala-2.6.1-final. Задайте этот каталог в переменной окружения SCALA_HOME и добавьте SCALA_HOME\bin к PATH для упрощения вызова из командной строки. Для проверки вашей инсталляции просто запустите "scalac -version". В ответ должно последовать "Scala version, 2.6.1-final".


Функциональные концепции

Прежде, чем мы начнем, я представлю несколько функциональных концепций, необходимых для понимания того, почему же Scala ведет себя так, а не иначе. Если вам доводилось иметь дело с функциональными языками - Haskell, ML или же с недавним пополнением в функциональном мире - F#, вы можете переходить к следующему разделу.

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

В отличие от многих динамичных языков, с недавних пор начавших отвоевывать себе пространство на платформе Java, Scala является статически типизированным, как и Java. Однако, в противовес платформе Java, Scala прилагает существенные усилия к использованию выведения типов ( type inferencing ), означающего, что компилятор выполняет глубокий анализ кода для выяснения типа конкретного значения, без вмешательства программиста. Выведение типа требует меньшей избыточности в типизации кода. Например, рассмотрим приведенный в листинге 1 Java-код, необходимый для объявления локальных переменных и присвоения им значений:

Листинг 1. Ох уж этот гениальный javac ...

                
class BrainDead {
  public static void main(String[] args) {
    String message = "Зачем указывать javac-у, что message - это строка?" +
      "А что же еще это может быть, если я и так присваиваю String?";
  }
}

Scala не нуждается в таком расписывании, как и будет показано далее.

Множество прочих функциональных особенностей (таких как сопоставление с шаблоном - pattern matching ) проложили себе путь в Scala, но полное их перечисление было бы забеганием вперед. Scala также добавляет некоторое количество деталей, отсутствующих на данный момент в Java, скажем, перегрузка оператора - operator overloading (являющаяся, как оказывается, тем, что Java-программисты вообще не могут себе вообразить), универсальные типы ( generics ) с "верхним и нижним ограничениями по типу", виды ( views ) и многое другое. Эти особенности, среди прочего, делают Scala чрезвычайно мощным для решения такого рода задач, как обработка или генерация XML.

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

Приступаем

Наша первая Scala-программа будет стандартной демонстрационной программой, Hello World, как того требуют Боги Компьютерных Наук:

Листинг 2. Hello.Scala

                
object HelloWorld {
  def main(args: Array[String]): Unit = {
    System.out.println("Hello, Scala!")
  }
}

Скомпилируйте это вызовом scalac Hello.scala и запустите полученный код при помощи либо запускающего модуля (scala HelloWorld), либо используя традиционный запуск Java, не забыв включить библиотеку ядра Scala в JVM classpath (java -classpath %SCALA_HOME%\lib\scala-library.jar;. HelloWorld). В любом случае должно появиться традиционное приветствие.

Некоторые элементы в листинге 2 определенно вам знакомы, но также задействованы и некоторые явно новые. К примеру, начав с привычного вызова System.out.println, Scala демонстрирует свою дружественность к лежащей в основе платформе Java. Scala преодолевает огромное расстояние, чтобы обеспечить доступность всей мощи Java в Scala-программах. (В действительности, позволительно даже наследовать тип Scala от Java-класса и наоборот, но об этом позже).

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

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

Ну а теперь давайте взглянем на то, в чем Scala действительно начинает отклоняться от традиционного Java/объектно-ориентированного кода.

Функция и форма - наконец-то вместе

Прежде всего, приверженцы Java отметят, что вместо "class", HelloWorld определен с использованием ключевого слова object. Это поклон Scala в сторону вездесущего шаблона Singleton - служебное слово object сообщает компилятору Scala, что это будет синглетон-объект, и в результате Scala гарантирует, что в любой момент времени будет существовать только один экземпляр HelloWorld. Обратите внимание - по той же причине main не определяется как статический метод, как это было бы в Java-программировании. Фактически Scala избегает использования "static" вообще. Если же приложению необходимо иметь экземпляры некоторого типа наряду с его "глобальным" вариантом, приложение Scala позволит как определение class так и object для одного и того же имени.

Далее, посмотрите на определение main, которое, как и в случае Java-кода, является общепринятой точкой входа для Scala-программ. Это определение, хотя оно и выглядит отличным от такового в Java, идентично: main принимает массив строк в качестве аргумента и ничего не возвращает. Тем не менее, в Scala такое определение несколько отличается от Java-версии. Определение параметра args задано как args: Array[String].

В Scala массивы представлены экземплярами обобщенного класса Array, кроме прочего демонстрирующего то, что Scala использует квадратные скобки ("[]") вместо угловых ("<>") как признак параметризованных типов. Ну и, для полноты картины, отметим использование в языке шаблона "имя: тип".

Как и в случае с другими функциональными языками, Scala требует, чтобы функции (в данном случае метод main) в обязательном порядке возвращали значение. Вот он и возвращает значение "не-значение", называемое Unit. В практическом смысле Java-разработчики могут думать о Unit как об аналоге void, по крайней мере, на данный момент.

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

Вы сказали замыкания?

Из того, что функции являются концепциями первого рода, вытекает необходимость представления их каким-либо образом в виде автономных конструкций, также известных как замыкания ( closures ) - понятие, столь горячо обсуждаемое последнее время Java-сообществом. В Scala это легко выполнимо. Прежде чем демонстрировать возможности замыканий, рассмотрим простую программу в листинге 3. Здесь функция oncePerSecond() повторяет свою логику (в данном случае - печать в System.out) каждую секунду.

Листинг 3. Timer1.scala

                
object Timer
{
  def oncePerSecond(): Unit =
  {
    while (true)
    {
      System.out.println("Time flies when you're having fun(ctionally)...")
      Thread.sleep(1000)
    }
  }

  def main(args: Array[String]): Unit =
  {
    oncePerSecond()
  }
}

Прим.пер.: Практически тройная игра слов: Time flies when you're having functionally - Время летит, пока вы "функциональничаете" Time flies when you're having fun - Время летит, пока вы бездельничаете Time flies when you're having functionally - Время летит, пока вы заняты делом

К сожалению, именно этот код вообще не является функциональным ... или хотя бы практичным. Например, если бы я захотел изменить текст выводимого сообщения, мне бы пришлось изменить тело метода oncePerSecond. Рядовой Java-программист определил бы в oncePerSecond параметр с типом String для передачи такого сообщения. Но даже такой подход крайне ограничен: Любая другая периодическая задача (допустим, пингование удаленного сервера) будет нуждаться в своей собственной версии oncePerSecond - что является прямым нарушением правила "Не повторяйся". Как показано в листинге 4, замыкания предлагают гибкую и мощную альтернативу:

Листинг 4. Timer2.scala

                
object Timer
{
  def oncePerSecond(callback: () => Unit): Unit =
  {
    while (true)
    {
      callback()
      Thread.sleep(1000)
    }
  }

  def timeFlies(): Unit = 
  { Console.println("Time flies when you're having fun(ctionally)..."); }

  def main(args: Array[String]): Unit =
  {
    oncePerSecond(timeFlies)
  }
}

Теперь ситуация становится интереснее. В листинге 4 функция oncePerSecond принимает параметр, но его тип выглядит странно. Формально, в качестве значения параметра callback принимается функция. Это справедливо до тех пор, пока передаваемая функция сама не имеет входных параметров (обозначено как "()"), и возвращает (обозначено "=>") "ничего" (функциональное значение "Unit"). Далее обратите внимание - в теле цикла я использую callback для вызова переданного в параметре объекта-функции.

К счастью, где-то в программе у меня есть такая функция, а именно timeFlies. Поэтому я просто передаю ее в oncePerSecond при вызове из main. (Вы также заметите, что в timeFlies задействован Scala-специфичный класс Console, служащий для той же цели, что и System.out или же новый класс java.io.Console. Это чисто эстетический вопрос; здесь будут работать и System.out и Console.)

Анонимная функция, какова же твоя функция?

Сейчас функция timeFlies выглядит как нечто бесполезное - после всех усилий у нее, на самом деле, нет другого назначения, как только быть переданной в oncePerSecond. Раз так, я бы вообще не хотел формально ее определять, как показано в листинге 5:

Листинг 5. Timer3.scala

                
object Timer
{
  def oncePerSecond(callback: () => Unit): Unit =
  {
    while (true)
    {
      callback()
      Thread.sleep(1000)
    }
  }

  def main(args: Array[String]): Unit =
  {
    oncePerSecond(() => 
      Console.println("Time flies... oh, you get the idea."))
  }
}

В листинге 5 функция main передает произвольный блок кода в качестве параметра oncePerSecond, это выглядит как лямбда -выражение из Lisp или Scheme, что, само по себе, является другой разновидностью замыкания. Такая анонимная функция опять демонстрирует мощь отношения к функциям как к гражданам "первого сорта", позволяя вам обобщать код таким совершенно новым способом, не прибегая к механизму наследования. (Поклонники шаблона Strategy, вероятно, уже начали неконтролируемо исходить слюной.)

Но на самом деле, oncePerSecond по-прежнему специфична: она завязана на неразумное ограничение в том, что callback будет вызываться каждую секунду. Я могу дальше продвинуться в обобщении, указав второй параметр, задающий - как часто вызывать переданную функцию, что и показано в листинге 6:

Листинг 6. Timer4.scala

                
object Timer
{
  def periodicCall(seconds: Int, callback: () => Unit): Unit =
  {
    while (true)
    {
      callback()
      Thread.sleep(seconds * 1000)
    }
  }

  def main(args: Array[String]): Unit =
  {
    periodicCall(1, () => 
      Console.println("Time flies... oh, you get the idea."))
  }
}

Это распространенная практика в функциональных языках: создать абстрактную функцию верхнего уровня, выполняющую некоторую работу, передать в нее блок кода (анонимную функцию) как параметр, и вызвать этот блок кода внутри высокоуровневой функции. Например, при переборе коллекции объектов. Вместо использования в цикле традиционного для Java объекта-итератора, функциональная библиотека предлагает взамен функцию - обычно называемую "iter" или "map" - в коллекционных классах она принимает функцию с одним параметром (объектом, подлежащим итерированию). Таким образом, например, упомянутый ранее класс Array содержит функцию filter, определенную в листинге 7:

Листинг 7. Часть листинга Array.scala

                
class Array[A]
{
    // ...
  	def filter  (p : (A) => Boolean) : Array[A] = ... // не показано
}

Листинг 7 декларирует, что p - функция, принимающая параметр обобщенного типа A и возвращающая логическое значение. Документация Scala утверждает, что filter "возвращает массив, состоящий из всех элементов исходного массива, удовлетворяющих предикату p." Это значит, что если я захочу на мгновение вернуться к моей программе Hello World и найти все аргументы командной строки, начинающиеся с буквы "G", это будет записано как в листинге 8:

Листинг 8. Привет, люди-G!

                
object HelloWorld
{
  def main(args: Array[String]): Unit = {
    args.filter( (arg:String) => arg.startsWith("G") )
        .foreach( (arg:String) => Console.println("Найдено " + arg) )
  }
}

Прим.пер.: G-man - агент ФБР

Здесь filter принимает предикат - анонимную функцию, неявным образом возвращающую логическое значение boolean (как результат вызова startsWith()) и вызывает этот предикат для каждого элемента в массиве "args". Если предикат возвращает истина, filter добавляет такой элемент в результирующий массив. После перебора всего массива, результирующий массив возвращается и немедленно используется в качестве исходного для вызова "foreach", который выполняет именно то, что предполагает: foreach принимает другую функцию и применяет ее к каждому элементу массива (в данном случае - просто отображает в консоль)

Не слишком сложно представить, как мог бы выглядеть Java-эквивалент рассмотренного выше кода и не слишком трудно признать, что версия Scala гораздо, гораздо короче и намного очевиднее.

Заключение

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=16215