Путеводитель по Scala для Java-разработчиков: Функциональное программирование вместо объектно-ориентированного (исходники)Источник: IBM developerWorks Россия Тед Ньювард
Вы никогда не забудете свою первую любовь. В моем случае ее звали Табинда (Бинди) Хан. Это были безмятежные годы моей юности, седьмой класс, если быть точным, а она была красива, смышлена и, что самое замечательное, она хохотала над моими неуклюжими подростковыми шуточками. Мы "запали" друг на друга (как это тогда называлось), со взлетами и падениями в отношениях, длившихся преимущественно в 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. Текущим на момент написания статьи релизом является 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. Задайте этот каталог в переменной окружения
|
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, как того требуют Боги Компьютерных Наук:
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) каждую секунду.
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() } } |
К сожалению, именно этот код вообще не является функциональным ... или хотя бы практичным. Например, если бы я захотел изменить текст выводимого сообщения, мне бы пришлось изменить тело метода oncePerSecond
. Рядовой Java-программист определил бы в oncePerSecond
параметр с типом String
для передачи такого сообщения. Но даже такой подход крайне ограничен: Любая другая периодическая задача (допустим, пингование удаленного сервера) будет нуждаться в своей собственной версии oncePerSecond
- что является прямым нарушением правила "Не повторяйся". Как показано в листинге 4, замыкания предлагают гибкую и мощную альтернативу:
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:
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:
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:
object HelloWorld { def main(args: Array[String]): Unit = { args.filter( (arg:String) => arg.startsWith("G") ) .foreach( (arg:String) => Console.println("Найдено " + arg) ) } } |
Здесь filter
принимает предикат - анонимную функцию, неявным образом возвращающую логическое значение boolean (как результат вызова startsWith()
) и вызывает этот предикат для каждого элемента в массиве "args
". Если предикат возвращает истина, filter
добавляет такой элемент в результирующий массив. После перебора всего массива, результирующий массив возвращается и немедленно используется в качестве исходного для вызова "foreach
", который выполняет именно то, что предполагает: foreach
принимает другую функцию и применяет ее к каждому элементу массива (в данном случае - просто отображает в консоль)
Не слишком сложно представить, как мог бы выглядеть Java-эквивалент рассмотренного выше кода и не слишком трудно признать, что версия Scala гораздо, гораздо короче и намного очевиднее.
Программирование в Scala подкупающе просто и в то же время - необычно. Его простота в том, что вы продолжаете работать с теми же базовыми объектами Java, знакомыми и любимыми вами на протяжении многих лет, а очевидное отличие - в способе, которым вам предлагается осмысливать нисходящую декомпозицию программы на части. В этой первой статье из серии Путеводитель по Scala для Java-разработчиков я лишь бегло ознакомил вас с возможностями Scala. То ли еще будет, а пока - успешного "функционализирования"!