среда, 28 января 2015 г.

Использование IntelliJ IDEA в качестве IDE для Scala, а также других функциональных языков программирования (http://eax.me/intellij-idea-scala/)

IntelliJ IDEA позиционируется, как IDE для программирования на множестве различных языков, не только Java. В частности, есть совершенно замечательный плагин для Erlang, которым я с огромным удовольствием пользуюсь уже около месяца. Еще я слышал, что есть плагин для Haskell, но пока что его не пробовал. Так вот, если с Erlang, к примеру, все довольно просто — открываешь проект и фигачишь, то со Scala есть пара нюансов, о которых мне и хотелось бы поведать.
Дополнение: Вскоре после написания этой заметки я все-таки нашел и попробовалплагин для Haskell. Поддержка этого языка оказалось самой базовой. Имеется подсветка синтаксиса, в том числе внутри cabal-файлов, работают простые вещи вроде перехода в пределах текущего файла к определению функции по нажатию F4, есть базовый автокомплит (Alt + /). До уровня полноценной IDE определенно не дотягивает. С другой стороны, как минимум, этот плагин не хуже, чем Vim. Еще в настоящее время активно разрабатывается альтернативный плагин HaskForce, но согласно описанию «this plugin is in its early stages and is not ready for use», поэтому я его даже не смотрел.
Как и все плагины, плагин для Scala устанавливается через File → Settings → Plugins → Browse repositories. Находим плагин с именем «Scala», устанавливаем, перезапускаем IDE. Там же можно поставить плагин для Erlang и других языков.
Теперь создадим новый проект. IntelliJ IDEA умеет работать как с Maven, так и SBT(как я понимаю, есть еще такая штука Gradle, которая также поддерживается). Коллеги-скалолазы пользуются Maven, в связи с чем он мне больше интересен. Поэтому далее речь пойдет о нем.
Если коротко, то после раскуривания StackOverflow и различных туториалов, у меня получился такой pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>me.eax.examples</groupId>
  <artifactId>screenshot-maker</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>
      UTF-8
    </project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.11.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <version>2.15.2</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
            <configuration>
              <args>
                <arg>-deprecation</arg>
              </args>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>

      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>
                me.eax.examples.screenshot_maker.ScreenshotMaker
              </mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
После сохранения изменений в pom.xml IntelliJ IDEA осознает, что в вашем проекте используется Scala, а также подтянет все нужные плагины для Maven. Алсо в проекте не повредит создать файл .gitignore. Воспользуемся тем же файлом, что приводился в заметке Функциональщик ботает Java — работа со сторонними библиотеками и хождение в РСУБД через JDBC:
# Eclipse
.classpath
.project
.settings/

# Intellij
.idea/
*.iml
*.iws

# Mac
.DS_Store

# Maven
log/
target/
Далее открываем окно Project и создаем каталог src/main/scala. Жмакаем по каталогу правой кнопкой мыши и говорим Mark Directory As → Sources Root. После этого каталог должен сменить цвет с оранжевого на синий, а в его контекстном меню станут доступны пункты для создания пакетов и классов (кстати, отчего бы сразу не создать парочку?). Аналогичным образом создаем каталог src/test/scala, только помечаем его как Test Sources Root. Если вы не планируете использовать в проекте Java, то каталоги src/*/java можно удалить. В результате должна получить примерно такая картинка:
Использование IntelliJ IDEA в качестве IDE для Scala
Как вы уже догадались, чтобы не писать банальный «Hello World», я решил написать простенькую программку для создания скриншотов.
Исходный код:
package me.eax.examples.screenshot_maker

import java.awt._
import java.io._
import javax.imageio._

object ScreenshotMaker extends App {
  def executableName : String = {
    System.getProperty("sun.java.command")
  }

  def makeScreenshot(fileName: String) {
    val toolkit = Toolkit.getDefaultToolkit
    val rect = new Rectangle(toolkit.getScreenSize)
    val capture = new Robot().createScreenCapture(rect)
    ImageIO.write(capture, "png"new File(fileName))
  }

  if(args.isEmpty) {
    println(s"Usage: ${executableName} screenshot.png")
  } else {
    makeScreenshot(args(0))
  }
}
Между прочим, приведенный выше скриншот был сделан при помощи этой самой программы, а затем отредактирован в Gimp.
Сочетание Shift + F10 не будет работать, пока вы не объявите класс, который «extends App». Что интересно, в IntelliJ IDEA есть поддержка REPL. В частности, Shift + F10 его и открывает. Также в REPL можно попасть нажатием Ctr + Shift + D или выделив кусок кода и нажав Ctr + Shift + X. В последнем случае выделенный код будет выполнен в REPL. Само собой разумеется, в REPL можно выполнять любой код, при этом работает автокомплит, просмотр документации (Ctr + Q), подсказка по аргументам (Ctr + P) и есть история. Это офигительно удобно! Запускается набранный в REPL код сочетанием Ctr + Enter.
Кстати, в Erlang’овом плагине вроде как тоже есть поддержка REPL, но там все как-то сложно. Я пока не осилил и работаю с REPL по старинке, из консоли.
Собрать standalone jar, как мы это уже когда-то выясняли, можно командой:
mvn clean compile assembly:single
Пара небольших советов.
Hint номер один. В IntelliJ IDEA можно создавать шаблоны проектов — Tools → Save Project as Template. Hint номер два. Несмотря на то, что IntelliJ IDEA умеет интегрироваться с Git, а также другими системами контроля версий, в реальных проектах с кучей веток и зависимостей намного удобнее работать с Git в консоли(Alt + F12). И это может относится не только к Git. При программировании на Erlang я пишу код локально, затем заливаю его на dev-сервер и собираю уже там — все это из консоли IntelliJ.
Общие впечатления от плагинов как для Scala, так и для Erlang, исключительно положительные. Сочетания клавиш, всякие там рефакторинги (включая краевые случаи вроде инлайнинга при использовании интерполяции строк), переходы к определению методов и так далее — в первом приближении все работает. Иногда, довольно редко, всплывают небольшие баги, но до сих пор не было ни одного критичного. И в любом случае, даже с этими багами писать код в IntelliJ IDEA оказалось намного удобнее, чем в Vim. Поначалу, само собой разумеется, немного непривычно. Но поработайте с этой IDE пару недель, и вам не захочется слезать ни на что другое.

Как будучи Java-программистом перестать сомневаться и перейти на Scala, а также о применимости Scala для Android (в соавторстве с Алексеем Пышненко) (http://eax.me/scala-doubts/)

Нам нередко приходилось сталкиваться с сомнениями касательно перехода на Scala людей, которые в настоящее время пишут на Java. И в самом деле, насколько вообще перспективно переучиваться с простой, понятной и всем известной Java на какую-то там странную и никому неизвестную Scala? Кроме того, ситуацию осложняет тот факт, что поверхностное гугление находит помимо положительных отзывов о Scala и весьма негативные.
Примечание: Заметка написана в соавторстве с Алексеем Пышненко.
Действительно, как и в случае с любой другой технологией, существуют как положительные, так отрицательные отзывы. Но отрицательный отзывы обычно появляются из-за относительной (например, по сравнению с Java или PHP) высотой порога вхождения в язык. Такие элементы функционального программирования, как неизменяемые переменные, строгая статическая типизация или сопоставление с образцом, многим программистам непонятны, потому что это не то, чему их учили в школе на примере Pascal. В то же время эти программисты чувствуют себя довольно комфортно в своих интерпрайзах, поэтому не испытывают особой мотивации разбираться. Отсюда и появляются отзывы типа «Scala сложная, как C++», хотя это вовсе не так.
По существу, единственный реальный недостаток Scala — это относительно медленная компиляция. Но на современном железе (типа MacBook Pro и сравнимых машинах) это уже и не является проблемой. Что же касается высокого порога вхождения в язык, наш опыт показывает, что заинтересованный программист, написавший пару hello world’ов на Scala, уже где-то через полтора месяца работы над реальным проектом пишет на языке совершенно свободно. При этом те же программисты отмечают существенно бОльшую скорость разработки на Scala по сравнению с Java, а также существенно меньшее количество ошибок, особенно связанных с многопоточностью — за счет неизменяемости переменных и модели акторов (библиотека Akka).
Scala используется в Twitter (у них практически все, ну кроме БД, написано на Scala), Netflix (обработка HTTP, и множество бизнес-логики на Scala), Foursquare(написан и работает на Scala, веб-интерфейс на Scala), LinkedIn (веб-интерфейс написан на Scala, возможно и внутри что-то тоже), The Guardian, Sony, Siemens, Xerox, Novell, а также в Яндексе и многих других российских, украинских и белорусских компаниях (можете проверить по HeadHunter). Ежегодно выходит множество книг, посвященных Scala. Существует довольно большое, активное и дружелюбное сообщество программистов, ведущих блоги, выступающих на конференциях, пишущих подкасты и общающихся в списках рассылок. Посмотрите хотя бы рассылки scala-user@akka-user@ и play-framework@ в группах Google.
Не стоит недооценивать важность комьюнити. Оно постоянно двигает язык вперед, и делает его все более мощным. На подходе Scala 3, которая сейчас называетсяDotty. Это язык с dependent types, которые являются мечтой любого знакомого с ними программиста, желающего писать корректные программы. Помимо ученых, которые, надо сказать, работают далеко не на голом энтузиазме, языком занимается компания Typesafe, в которую не так давно было вложено немало инвестиций, самые значимые из которых составили 14 миллионов долларов. И они оправдывают эти инвестиции — популярность языка Scala растет с каждым днем, постоянно появляются все новые и новые вводные материалы для людей, заинтересованных, но не знакомых с языком, читаются доклады по теории типов на различных конференциях.
Что же касается опасения в отношении новых технологий, то его трудно не понять. Но, по всей видимости, в данном случае программист абсолютно ничего не теряет. Программируя на Scala, вы пишите под ту же JVM. Код на Scala даже может быть использован в коде на Java и наоборот. Притом несложно понять, как код на Scala транслируется в Java-классы, то есть вы просто пишите на Java с более приятным синтаксисом (кортежи, автоматический вывод типов). Те же Play, Akka и другие библиотеки успешно применяются программистами на Java. Так что, вы получаете универсальный опыт, применимый как для Scala, так и для Java.
Следует также отметить возможность разработки на Scala под Android.
Если вы когда-нибудь слышали про RxJava, то там представлен ровно тот подход, вокруг которого построена Scala — reactive programming. Это такой подход к event-driven архитектуре приложения, нацеленный на асинхронность во всем. В итоге все ваше приложение выглядит как совершенно прозрачная цепочка событий, где отлично видно причину и последствия любого действия пользователя. В итоге при какой-либо ошибке в бизнес-логике почти сразу же ясно, что пошло не так: отладка почти не отнимает времени. Ко всему прочему, код становится намного более компактным и выразительным, количество возможностей для обобщения логики разительно возрастает по сравнению с Java, при этом не теряя ни единой возможности, которую предоставляет Java.
Вот что говорят наши Android-программисты о достаточно скором для них переходе на Scala.
Очень заметно, как скорость разработки растет. Несмотря на то, что мы только вникаем в Scala, и не всегда все делаем правильно, а также тратим часть времени на обучение, скорость создания отдельного компонента / сущности / экрана заметно возросла, и продолжает расти.
В Scaloid, которым пользуется теперь наша команда, много очень удобных оберток над стандартными компонентами (SButton, SListView, и тд), которые сильно упрощают жизнь. Контекст теперь передается имплицитно, обработчики различных событий зачастую указываются прямо в конструкторе (имеется ввиду метод apply у companion object). Работа с коллекциями, несмотря на множество отзывов об их сложности со стороны опытных скалистов, стала намного приятнее. Теперь то, что в Java нужно было долго и упорно описывать, делается в несколько строк — map, flatMap, reverse, fold, что еще нужно?
Future сильно упростили код. AsyncTask’и хоть и вполне легковесны для Java, но ни в какое сравнение с легкостью и читабельностью Future они не идут. Код, обернутый во Future, отлично вписывается в код, работающий с UI. При чтении никакого переключения контекста не происходит, чего нельзя сказать о выделении собственного потока или использования AsyncTask.
Вот как можно сократить Java-код (взято отсюда):
private class LongOperation extends AsyncTask<StringVoid, String> {
    @Override
    protected String doInBackground(String... params) {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.interrupted();
            }
         }
         return "Executed";
     }

    @Override
    protected void onPostExecute(String result) {
        System.out.println("OnPostExecute");
    }

    @Override
    protected void onPreExecute() {
        System.out.println("OnPostExecute");
    }

    @Override
    protected void onProgressUpdate(Void... values) {}
}
… переписав его на Scala:
Future {
    println("OnPreExecute")
    for (<1 to 5) {
        Thread.sleep(1000)
    }
    "Executed"
}.map(_ => "OnPostExecute")
 .recover { case e => println(s"Error $e"); "OnError"}
В общем и целом, Scala сильно упростила разработку под Android. Больше нет ощущения борьбы с языком, которое иногда возникало в Java. Все просто идет своим чередом, одно вытекает из другого, все логично и красиво, и это здорово.

Мы надеемся, что данная заметка помогла вам развеять хотя бы часть сомнений по поводу Scala. Будем рады любым вашим комментариям в отношении написанного, а также ответным постам.
P.S. И кстати, даже Bruce Eckel любит Scala :)

Scala. Введение (http://habrahabr.ru/post/77750/)

Не так давно я заинтересовался одним из многочисленных ныне языков под JVM — Scala. Причин тому много, основная — всё нарастающее со временем чувство неудобства при работе с cpp-подобными языками. Взгляд мой попеременно падал на Ruby, Groovy, Python, но все они оставляли впечатление инструментов, не совсем подходящих для моего обычного круга рабочих задач (Python-таки хорош, но у нетипизированных языков есть свои ограничения). Scala же, напротив, показалась вполне годным языком. Так как поиск по хабру никаких статей о ней не выловил (было несколько, но мягко говоря не вводных), я решил написать маленький обзор и поделиться им с массами.

Немного философии языка в вольном изложении


Какие основные цели преследовали создатели языка? Согласно моим мироощущениям они такие:
Во-первых, совместимость. Среди своих задач разработчики ставили поддержание совместимости с Java-языком и тысячами примеров говнокода разработок на ней для решения самых разнообразных задач.
Во-вторых, интенсивное насыщение языка функциональными фичами, которые, в основном, (но далеко не полностью) составляют его отличия от Java.
В-третьих, облегчение нашего с вами труда. Действительно, компилятор Scala понимает программиста с полуслова, писать код специально, чтобы втолковывать ему, что я не верблюд, мне не довелось пока.
В-четвёртых, поддержка и стимулирование написания модульных, слабосвязанных программных компонентов в сочетании с широкими возможностями адаптации уже существующих. Цели не то, чтобы совсем противоположные, но порождающие известные трудности для одновременного достижения. Что ж, посмотрим что получится.
В-пятых, это поддержка параллелизма. К сожалению у меня руки и голова до этой области не дошли (надеюсь пока), но акцент на этом моменте делается постоянно на всех ресурсах по языку.

Для экспериментов с языком достаточно поставить соответствующий плагин на любимую IDE отсюда.

Итак, давайте посмотрим на сам язык…

Общие идеи языка, примеры синтаксиса


Самое, пожалуй, важное, — это «унифицированная модель объектов». Этот термин расшифровывается авторами так: «каждое значение — объект, каждая операция — вызов метода». Это, конечно, не «всё — объект», но сущностей в сравнении с Java убыло, а чем меньше сущностей — тем легче жизнь :) В прикладном плане это означает, что числа и символы сделались неизменяемым объектами, обитающими в общей куче, все операции приобретают ссылочную семантику. Например, код 5 + 5 вполне валиден, и породит новый объект в куче, который оперативненько оприходует сборщик мусора (на самом деле, я тихо надеюсь, что компилятор поймёт глубину замысла и порождать он ничего не будет :) ).

После столь возвышенного введения можно глянуть на решение классической задачи:
object Main {
 def main(args:Array[String]) :Unit = {
  print("Hello, " + args(0) + "!")
 }
}

В нём мы видим следующее:
  • Можно объявлять отдельные объекты. Ничего необычного в этом нет, подобная возможность имеется, например в Groovy. Ведут себя такие объекты так же как написанные на Java реализации шаблона Singelton.
  • Объявление функции выглядит непривычно, но вполне читабельно: [ключевое слово def] [имя]([список параметров]):[возвращаемый тип] = [блок кода].
  • В качестве типа, не несущего информационной нагрузки, выступает тип Unit. Он вполне аналогичен void в C-подобных языках.
  • Объявление параметра функции (а на самом деле и локальной переменной тоже) выглядит как [имя]:[тип].
  • Для параметризации типа используется не привычные нам <>, а казалось бы, навсегда закреплённые за массивами [].
  • Для обращения к элементам массива(экое непотребство) используются ().
  • Имеется какие-то встроенные функции, доступные в коде по умолчанию без всяких дополнительных импортов.

В дополнение, давайте взглянем на ещё один короткий пример:
println( ("Hello, " + args(0) + "!").toUpperCase )
println( "Hello, " + args(0) + "!" toUpperCase )

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

В качестве подспорья разработчику Scala поддерживает также интерактивный режим. То есть, можно запустить интерпретатор и по одной вводить команды. Интерпретатор, встроенный в IDE, как-то нерегулярно работает, его отдельный вариант есть в репозиториях Убунты, думаю у остальных дистрибутивов тоже всё хорошо, счастливым обладателям Windows как всегда придётся помучаться :) Интерпретатор запускается самым необычным способом:
$ scala
Welcome to Scala version 2.7.3final (Java HotSpot(TM) Server VM, Java 1.6.0_16).
Type in expressions to have them evaluated.
Type :help for more information.
scala>

Совсем маленький пример:
scala> 1 to 10
res0: Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Тут мы видим пример вызова метода с параметром. Если кто не догадался, у объекта класса Int 1 вызывается метод to с параметром того же типа 10, результат — диапазон значений.

Попробуем-ка мы теперь написать ещё одну функцию. Пусть она нам считает сумму чисел в заданном диапазоне, итак:
scala> def sum(a: Int, b: Int): Int = {
| var result = 0
| for (i <- a to b) result += i
| result
| }
sum: (Int,Int)Int

scala> sum(1, 5)
res3: Int = 15

Здесь видны ещё три важных момента:
  • При помощи ключевого слова var мы можем объявлять локальные переменные
  • Результатом вычисления блока является последнее выражение в нём
  • В нашем распоряжении имеется цикл for, который может выполнять вычисления для значений в заданном диапазоне (на самом деле для объектов в любом объекте — контейнере)

Операции над функциями


Что же мы такого можем с ними тут делать? Да что угодно =) Функции являются полноценными объектами программы. Их можно хранить как свойства объектов, передавать как параметры и возвращаемые значения и собственно создавать во время выполнения. Данные свойства позволяют строить так называемые функции высокого порядка, оперирующие себе подобными.

Для иллюстрации рассмотрим ставший классическим пример вычисления суммы:
scala> def sum(f: Int => Int, a: Int, b: Int): Int =
| if (a > b) 0 else f(a) + sum(f, a + 1, b) sum: ((Int) => Int,Int,Int)Int

В данном примере определяется функция sum, представляющая знакомый, надеюсь, всем оператор суммы. Параметры имеют следующий смысл:
f — функция преобразования целого числа из пределов суммирования в элемент суммы. Обратите внимание на объявление типа параметра: знак => означает, что параметр — функция, типы принимаемых значений перечисляются слева от него в круглых скобках (если параметр один, как в данном примере, их допустимо опустить), тип возвращаемого результата справа.
Работает она тривиально: вычисляет значение функции в нижней границе диапазона и складывает его с результатом вычисления себя самой в диапазоне на 1 меньшем.
Также в этом примере видна ещё одна особенность языка — if является выражением, имеющим значение (кстати, использованный ранее for — тоже выражение, его результат типа Unit). Если условие истина, то его результат первый вариант, иначе — второй.
a и b — пределы суммирования.

Ещё пара функций id и square, они равны своему параметру и его квадрату соответственно.
scala> def id(x: Int): Int = x
id: (Int)Int
scala> def square(x: Int): Int = x * x
square: (Int)Int

Тут надо сделать ещё одно лирическое отступление: функции в Scala имеют декларативный стиль объявления. Они описывают не как получить результат, а чему он равен. Но если требуется организовать последовательные вычисления в теле функции, нет проблем — у нас есть блоки.

Теперь можно воспользоваться тем, что мы написали ранее.
scala> sum(id, 1, 5)
res1: Int = 15
scala> sum(square, 1, 5)
res2: Int = 55

Здесь происходит кульминация этой части — мы берём и передаём функцию в другую функцию. Никаких интерфейсов, анонимных классов, делегатов: вот оно — маленькое счастье.

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

Особенности классов


Давайте опишем несложный класс. Пусть это будет комплексное число. Создадим следующий код:
class Complex(r: Double, i: Double) {
 def real = r
 def image = i
 def magnitude = Math.sqrt(r*r + i*i)
 def angle = Math.atan2(i, r)

 def + (that: Complex) = new Complex(this.real + that.real, this.image + that.image)

 override def toString = real+" + i*"+image+" | "+magnitude+"*e^(i*"+angle+"))"
}

object Main {
 def main(args:Array[String]) :Unit = {
  val first = new Complex(1, 5)
  val second = new Complex(2, 4)
  val sum = first + second
  println(first)
  println(second)
  println(sum)
 }
}

Во-первых, класс объявлен с какими-то параметрами. Как несложно догадаться по продолжению, это параметры конструктора, которые доступны всё время жизни объекта.
Во-вторых, в классе объявлено несколько методов — селекторов. Одно семейство для декартового представления и одно для полярного. Как видим оба они используют параметры конструктора.
В-третьих, в классе объявлен оператор сложения. Объявлен он как обычный метод, принимает также Complex и возвращает его же.
Ну и наконец, для этого класса переопределена, без сомнения, знакомая всем Java-программистам функция toString. Важно отметить что на переопределение методов в Scala всегда необходимо явно указывать при помощи ключевого словаoverride.

Несмотря на огромную практическую ценность данный класс обладает рядом недостатков, а именно:
  • Занимает неоправданно много для своей функциональности места на экране
  • Не умеет сравнивать себя с себе подобными

Что же, попробуем исправить недочёты, средствами этого прекрасного языка.
class Complex(val real: Double, val image: Double) extends Ordered[Complex] {
 def magnitude = Math.sqrt(real*real + image*image)
 def angle = Math.atan2(image, real)
 def + (that: Complex) = new Complex(this.real + that.real, this.image + that.image)
 def compare(that: Complex): Int = this.magnitude compare that.magnitude
 override def toString = real+" + i*"+image+" | "+magnitude+"*e^(i*"+angle+"))"
}

object Main {
 def main(args:Array[String]) :Unit = {
  val first = new Complex(1, 5)
  val second = new Complex(2, 4)
  if (first > second )
   println("First greater")
  if (first < second )
   println("Second greater")
  if (first == second )
   println("They're equal")
 }
}

Итак, что появилось нового:
  • У параметров конструктора появилось ключевое слово val и исчезли соответствующие селекторы. Да, всё вполне очевидно, это разрешение компилятору создать селекторы для них автоматически.
  • Добавилось наследование от незнакомого нам класса (а точнее trait'а) Ordered. Да не простого, а параметризованного нашим классом. Как следует из названия, он должен помочь нам с упорядочиванием наших экземпляров.
  • Появился метод compare, который сравнивает два комплексных числа посредством сравнения их модулей.
  • В тестовом методе появились использования операторов >, <, ==. Их мы явно не определяли.

Пришло время сказать несколько слов об идее trait'а. Это особый тип класса, который не может иметь конструкторов, но может иметь любые методы и атрибуты. Обычно они устанавливают некоторый протокол для взаимодействия со своими возможными наследниками. Используя этот протокол они могут получить необходимую информацию от потомка и реализовать в нём некоторое поведение. Соответственно, любой класс (или объект) может наследоваться от произвольного количества trait'ов(и лишь от одного class'а). Например Ordered объявляет абстрактный метод compare и на его основании дополняет класс-наследник операторами <, <=, > и т.д. Здесь надо заметить, что по-хорошему стоит переопределить предоставленный нам оператор ==, так как он даёт истину и для неодинаковых объектов, да и методы equals с hashCode также стоит переопределять в таких случаях.
«Всё это хорошо» — скажет бывалый боец аутсорсного рынка, «но что делать если требуется банальный domain-класс, с богомерзкими модификаторами атрибутов?».
Решение у нас, естественно, есть :)
class User {
 private[thisvar _name: String = ""
 def name = _name toUpperCase
 def name_=(name: String) = {
  _name = if (name != null) name else ""
 }
}

  • Во-первых, этот класс использует уже знакомое нам ключевое слово var в своём теле, да не просто а с диковинным модификатором private[this]. Значение этого ключевого слова в теле класса абсолютно аналогично таковому в внутри блока(и даже, скажу по секрету, в конструктор его тоже можно запихать) и делает из имени после него изменяемый атрибут класса. Диковинный модификатор заявляет, что переменная должна быть доступна только данному объекту. Можно было написать, например, private[User] и она стала бы доступна другим, нам подобным, объектам, или указать имя пакета (что-то это мне напоминает).
  • Далее объявлена функция возвращающая наше поле в верхнем регистре.
  • И в заключение, странная функция name_=, получающая строку в виде параметра, проверяющая что она не null и записывающая её в наше поле.

Чтобы понять, как это всё использовать давайте взглянем на результат выполнения следующего кода(для краткости я не стал включать сюда описание объекта и main-метода):
val user = new User("Scala!!!")
println(user.name)
user.name = "M. Odersky"
println(user.name)

SCALA!!!
M. ODERSKY


Внимание, вывод: метод с именем <что-то>_= вызывается при использовании конструкции <объект>.<что-то> = <что-то другое>. Насколько я знаю в Scala это второй хак (первый — преобразование () в вызов метода apply), как Гвидо завещалc неявным преобразованием использования оператора в вызов метода.

Pattern-matching


Начать придётся немного издалека. В Scala есть так называемые сase classes(естественно и objects тоже). Они объявляются с ключевым словом case, после чего компилятор берёт на себя смелость сделать следующее:
  1. Создать функцию-конструктор с именем, совпадающим с клаcсом.
  2. Имплементировать в классе toString, equals, hashCode на основе аргументов конструктора.
  3. Создать селекторы для всех аргументов конструктора.

Вся эта магия открывает нам путь к использованию метода match. Давайте взглянем на пример:
abstract class User

case class KnownUser(val name: String) extends User

case class AnonymousUser() extends User

object Test {
 val users = List(KnownUser("Mark"), AnonymousUser(), KnownUser("Phil"))

 def register(user: User): Unit = user match {
   case KnownUser(name) => println("User " + name + " registered")
   case AnonymousUser()     => println("Anonymous user can't be registered")
 }

 def main(args: Array[String]) =
  users.foreach( register )
}

Итак, общая картина кода: есть абстрактный класс пользователя, есть два его казуальных потомка: известный и анонимный пользователи. Мы хотим зарегистрировать некий список пользователей на (здесь включаем фантазию) встречу. Для чего и используем pattern-matching, который позволяет нам определить разное поведение метода для разных типов объектов и обеспечивает выборку данных из этих объектов.

После столь жизненного примера можно немного теории о работе метода match. Для каждого выражения case он выполняет проверку на совпадения типа с классом шаблона и соответствия параметров конструктора шаблону. Шаблон в общем случае может включать в себя:
  1. Конструкторы других case-классов. Тут всё вполне рекурсивно, глубина вложенности шаблона ограничивается безумием программиста не ограничивается.
  2. Переменные шаблона. Они становятся доступны в теле функции вычисления результата.
  3. Символы _ обозначающие любое, неинтересующее нас значение.
  4. Литералы языка. Например 1 или "Hello".

Таким образом мы получаем инструмент, позволяющий описать получение некоторого значения из объекта на основе его структуры (класса) и/или хранимых в нём данных.

Люди знакомые с базовыми принципами ООП конечно сразу заметят, что проблема эта вполне решается использованием виртуальных функций (более того предлагаемый подход является не лучшей практикой). Однако их использование несёт в себе две трудности: во-первых усложняет поддержку кода при большом числе таких функций (нам ведь захочется регистрировать пользователей и на события, и в группы, и в блоги и т.п., что для каждого случая создавать виртуальный метод?), во-вторых не решает проблемы с тем, что объекты одного типа могут принципиально иметь разную структуру и не иметь возможности предоставлять некоторые данные.

На вторую проблему хочется обратить особое внимание. Как выглядил бы приведённый выше код в Java? Один класс, если пользователь анонимный выставляем в имени null и проверяем каждый раз (эстеты вроде меня заводят методы типаisAnonymous, состоящие из сравнения поля с тем же null). Проблемы налицо — неявно и небезопасно. Таких примеров великое множество, когда разные вариации структуры объектов объединяются в один класс, а неиспользуемые в конкретном случае забиваются null'ами, или того хуже придумывается значение по умолчанию. Scala позволяет явно описывать вариации структуры объектов, и предоставляет удобный механизм для работы с этими вариациями.

В заключение, пара мыслей насчёт того, когда данная техника может быть эффективно применена как замена виртуальным функциям:
  • У нас много функций. Да если у нас пара сотен операций, используемых по паре раз, зависящих от структуры и содержания объектов, система на основе case classes — pattern matching будет явно лучше поддерживаема.
  • У нас мало классов. match из пары выриантов всегда хорошо читаем.
  • У нас есть значительные вариации структуры объектов, которые однако надо хранить и обрабатывать единообразно.

Для всех пунктов действуют и обратные утвердения, например использование pattern matching для десятка классов не кажется мне хорошей идеей.

Вывод типов


Думаю, вы уже заметили, что в коде я указывал типы только при объявлении классов и методов. В блоках кода я их практически всегда опускал. Дело в том, что если программист не указывает тип явно, Scala пытается определить его из контекста. Например при инициализации значения константы в определении def s = "Scala" компилятор определит тип константы как строку. Всё это также работает на обобщённых типах, например фрагмент выше val users = List(KnownUser("Mark"), AnonymousUser(), KnownUser("Phil")), создаёт константу типа List[User], автоматически поднимаясь до подходящего уровня в иерархии наследования и используя его для параметризации типа-контейнера. На практике это означает, что можно значительно сэкономить на подобных объявлениях (для развлечения напишите делающий то же самое код на Java или C# :) ).

Заключение


Мда… К началу поста уже и скролить долго. Явно пора заканчивать. А сказать хотелось бы ещё про многое: про интереснейший механизм описания обобщённых классов, про неявные преобразования и то, что они на самом деле явные, ленивую инициализацию констант.

Мне и самому ещё только предстоит изучить модель многопоточности и своеобразный набор примитивов для её реаизации, разобраться с языковой поддержкой xml, поиграться с DSL-строением, посмотреть на их флагманский проект — Lift…

Однако всё равно осмелюсь сделать пару выводов:
  • Scala является весьма лаконичным и выразительным языком
  • Она предоставляет мощный инструментарий для создания простых и красивых программ