(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Путеводитель по 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. То ли еще будет, а пока - успешного "функционализирования"!



 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 25.05.2008 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Купить WinRAR : 5 : Академическая лицензия 1 лицензия
TeeChart for .NET with source code single license
ABBYY Lingvo x6 Европейская Домашняя версия, электронный ключ
Quest Software. TOAD for Oracle Edition
SAP CRYSTAL Reports 2013 WIN INTL NUL
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
СУБД Oracle "с нуля"
Один день системного администратора
Программирование на Visual Basic/Visual Studio и ASP/ASP.NET
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100