Скала язык программирования

Scala. Введение

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

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

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

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

Самое, пожалуй, важное, — это «унифицированная модель объектов». Этот термин расшифровывается авторами так: «каждое значение — объект, каждая операция — вызов метода». Это, конечно, не «всё — объект», но сущностей в сравнении с Java убыло, а чем меньше сущностей — тем легче жизнь 🙂 В прикладном плане это означает, что числа и символы сделались неизменяемым объектами, обитающими в общей куче, все операции приобретают ссылочную семантику. Например, код 5 + 5 вполне валиден, и породит новый объект в куче, который оперативненько оприходует сборщик мусора (на самом деле, я тихо надеюсь, что компилятор поймёт глубину замысла и порождать он ничего не будет 🙂 ).
После столь возвышенного введения можно глянуть на решение классической задачи:
object Main {
def main(args:Array) :Unit = {
print(«Hello, » + args(0) + «!»)
}
}
В нём мы видим следующее:

  • Можно объявлять отдельные объекты. Ничего необычного в этом нет, подобная возможность имеется, например в Groovy. Ведут себя такие объекты так же как написанные на Java реализации шаблона Singelton.
  • Объявление фукции выглядит непривычно, но вполне читабельно: (): = .
  • В качестве типа, не несущего информационной нагрузки, выступает тип 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) :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 {
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) :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 var _name: String = «»
def name = _name toUpperCase
def name_=(name: String) = {
_name = if (name != null) name else «»
}
}

  • Во-первых, этот класс использует уже знакомое нам ключевое слово var в своём теле, да не просто а с диковинным модификатором private. Значение этого ключевого слова в теле класса абсолютно аналогично таковому в внутри блока(и даже, скажу по секрету, в конструктор его тоже можно запихать) и делает из имени после него изменяемый аттрибут класса. Диковинный модификатор заявляет, что переменная должна быть доступна только данному объекту. Можно было написать, например, private и она стала бы доступна другим, нам подобным, объектам, или указать имя пакета (что-то это мне напоминает).
  • Далее объявлена функция возвращающая наше поле в верхнем регистре.
  • И в заключение, странная функция 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. Создать функцию-конструктор с именем, совпадающим с класом.
  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) =
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, автоматически поднимаясь до подходящего уровня в иерархии наследования и используя его для параметризации типа-контейнера. На практике это означает, что можно значительно сэкономить на подобных объявлениях (для развлечения напишите делающий то же самое код на Java или C# 🙂 ).

Заключение

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

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

Вот и всё. Критика приветствуется.
Напоследок вопрос к массам: интересна ли данная тема, стоит ли писать продолжение?
UPD: поправил грамматику, спасибо всем оказавшим в этом помощь. Особенно ganqqwerty за массовые разборки с запятыми.

_________

Краткая справка

Scala была создана в начале 2000-ых годов в Федеральной политехнической школе города Лозанна (в Швейцарии). Руководителем команды, создавшей этот язык, был Мартин Одерски — знаменитый исследователь в области компьютерных наук. В январе 2004 года язык был выпущен для платформы JVM, а в июне того же года для .NET Framework (хотя поддержка для .NET была прекращена примерно в 2012 году). В 2016 году выпустили LLVM-компилятор.

В 2011 году Мартин Одерски основал Typesafe Inc(позднее она стала называться Lightbend Inc). На гранты, выделенные его команде для развития языка, компания осуществляет поддержку Scala-разработчиков и разрабатывает сервисы на этом языке, а также занимается продажей Scala-библиотек.

На язык оказали большое влияние другие языки, особенно много концепций было взято из Java и C#. Также были сделаны некоторые заимствования из Smalltalk, Ocaml и Beta.

Многие программисты и исследователи считают Scala следующей ступенью развития Java, особенно после того, как развитие последней затормозилось в связи с приобретением прав на неё компанией Oracle.

Философия Scala

Scala объединяет три подхода: статическую типизацию, ООП и функциональный подход. Это позволяет разрабатывать как маленькие скрипты, так и крупные распределенные системы.

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

Интеграция с Java позволяет компилировать код, написанный на Scala, для JVM, а также использовать все java-библиотеки, созданные за всё время существования Java. Кстати, такая совместимость также позволяет писать приложения под Android. А компиляция в JavaScript дает возможность создавать веб-приложения (бэкенд).

В отличии от Java, в Scala изначально были добавлены лямбда-выражения, монады и другие элементы ФП. Хотя в Scala функциональный подход реализован не настолько полно, как в Haskell или Erlang, но зато в нем скомбинированы функциональный и объектно-ориентированный подходы, позволяя сочетать их.

Плюсы/минусы Scala

Из плюсов:

  • Перспективность. Рано или поздно, Scala может занять место Java, поэтому будет мудро начинать сразу с неё, чтобы не отставать от передних рубежей разработки в будущем;
  • Востребованность. Сейчас на Scala достаточно много вакансий (хоть и не так много, как для Java). Вы можете найти достаточно должностей для Scala-разработчика как в России, так и в иностранных компаниях;
  • Поддержка функционального программирования;
  • По производительности не уступает Java;
  • Совместимость с Java-библиотеками;
  • Хорошо подходит для больших команд разработчиков (благодаря статической типизации);
  • Синтаксис Scala проще, чем у Java, поэтому его хвалят за удобство написания кода;
  • Возможность написания приложений под Android (написание приложений под iOS теоретически возможно, однако считается довольно непрактичным).

Минусы:

  • Высокий порог вхождения. Обучиться на Scala будет достаточно сложно, но это связано скорее с тем, что на нем нужно решать более сложные задачи, связанные с Big Data и большими системами. Также требуются определенные знания по языку Java;
  • Плохая поддержка со стороны IDE (например, в Eclipse. Хотя с развитием языка ситуация постепенно исправляется);
  • Непривычная для опытных разработчиков, перешедших с других языков, реализация ООП и функционального программирования;
  • Не подходит для системного программирования (там, где обычно используется язык C). Также Scala не используется для создания десктопных приложений;
  • Более низкая скорость компиляция (чем, например, в Go).

Scala (язык программирования)

У этого термина существуют и другие значения, см. Scala.

Scala

Класс языка

Мультипарадигмальный: функциональный, объектно-ориентированный, императивный

Появился в

Автор

Мартин Одерски

Разработчик

Programming Methods Laboratory of EPFL

Выпуск

2.12.7 (27.09.2018)

Система типов

статическая, строгая, с автовыведением типов, структурная

Испытал влияние

Java, Haskell, Erlang, Standard ML, Objective Caml, Smalltalk, Scheme, Algol68, Lisp

Повлиял на

Kotlin, Swift

Лицензия

Платформа

Java Virtual Machine, JavaScript и Native

Сайт

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

Первые версии языка созданы в 2003 году коллективом лаборатории методов программирования Федеральной политехнической школы Лозанны под руководством Мартина Одерски, язык реализован для платформ Java и JavaScript. По мнению Джеймса Стрэчена, создателя языка программирования Groovy, Scala может стать преемником языка Java.

История

Язык был создан в 2001—2004 годах в Лаборатории методов программирования EPFL. Он стал результатом исследований, направленных на разработку улучшенной языковой поддержки компонентного программного обеспечения. За основу при разработке языка были взяты две идеи:

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

Язык был выпущен для общего пользования на платформе JVM в январе 2004 года и на платформе .NET в июне 2004 года, в 2016 году создан LLVM-компилятор (Scala Native).

Истоки дизайна

На дизайн языка оказали влияние многие языки и исследовательские работы.

Прежде всего, язык впитал значительное число концепций и синтаксических соглашений Java и C#. Способ выражения свойств во многом заимствован из Sather. Из Smalltalk взята концепция унифицированной объектной модели. Из BETA пришла идея, что всё, включая классы, должно допускать вложенность. Абстрактные типы в Scala очень похожи на абстрактные типы сигнатур в SML и OCaml, обобщённые в контексте полноценных компонентов.

В некотором смысле Scala — это продолжение работы Pizza. Как и Pizza, Scala компилируется под Java VM, добавляя функции высшего порядка, сопоставление с образцом, конструкции, которые исходно были созданы в сообществе функционального программирования. В то время как Pizza обратно совместима с Java, цель Scala — всего лишь возможность взаимодействия, так что у неё больше степеней свободы в дизайне. Ещё одна цель Scala — предоставить расширенные конструкции для абстракции и композиции компонентов — общая с несколькими недавними исследовательскими разработками.

Ключевые аспекты языка

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

В Scala включены мощные и единообразные концепции абстракций как для типов, так и для значений. В частности, язык содержит гибкие симметричные конструкции примесей для композиции классов и типажей. Возможно позволяет производить декомпозицию объектов путём сравнения с образцом; образцы и выражения при этом были обобщены для поддержки естественной обработки XML-документов. В целом, эти конструкции позволяют легко выражать самостоятельные компоненты, использующие библиотеки Scala, не пользуясь специальными языковыми конструкциями.

Язык допускает внешние расширения компонентов с использованием представлений (views). Возможности обобщённого программирования реализуются за счёт поддержки обобщённых функций (generics), в том числе высшего типажа (generics of a higher kind). Кроме различных классических структурных типов данных, в язык включена поддержка экзистенциальных типов.

Объектно-ориентированный язык

В языке используется чистая объектно-ориентированная модель, похожая на применяемую в Smalltalk: каждое значение — это объект, и каждая операция — это отправка сообщения. Например, сложение x+y интерпретируется как x.+(y), то есть как вызов метода + с аргументом y и x в качестве объекта-приёмника.

Рассмотрим другой пример: 1+2. Это выражение интерпретируется как (1).+(2). Обратите внимание, что скобки вокруг чисел обязательны, потому что лексический анализатор Scala разбивает выражение на лексемы по принципу самого длинного возможного сопоставления. Таким образом, выражение 1.+(2) разобьется на лексемы 1.,+ и 2, потому что лексема 1. длиннее лексемы 1 и первый аргумент сложения будет интерпретирован, как тип Double вместо Int.

Функциональный язык

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

Повторное использование и адаптация

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

Scala представляет новую концепцию решения проблемы внешней расширяемости — представления (views). Они позволяют расширять класс новыми членами и типажами. Представления в Scala в некотором смысле соответствуют классам типов, используемым в Haskell, но в отличие от классов типов, область видимости представлений можно контролировать, причём в разных частях программы могут сосуществовать параллельные представления.

Примеры программ

Программа, как и в Java, представляет собой класс. Это пример консольной программы, которая выводит строчку текста на экран.

object HelloWorld { def main(args: Array) = println(«Привет, МИР!») } // Более короткая версия object HelloWorld extends App { println(«Привет, МИР!») }

Следующий простой пример программы написан на Java, Scala и C#, демонстрируя некоторые различия в синтаксисе (постфиксная запись типов переменных, отсутствие специального синтаксиса для доступа к массивам). В этом примере описывается консольная программа, которая выводит все опции, переданные через командную строку. Опции начинаются с символа «-» (минус).

В Scala объявляется не класс объекта, а сразу экземпляр объекта. Так естественным способом реализуется шаблон проектирования, где в программе должен быть только один экземпляр класса («Одиночка» — «Singleton»).

Пример программы, которая суммирует все элементы списка, который передаётся через аргументы:

object Main { def main(args: Array) { try { println(«Сумма аргументов: » + args.map(_.toInt).sum) } catch { case e: NumberFormatException => println(«Ошибка в аргументах. Использовать следует так: scala Main <число1> <число2> … «) } } }

На Java:

public class Main { public static void main(String args) { try { System.out.println(«Сумма аргументов: » + Arrays.stream(args).mapToInt(Integer::parseInt).sum()); } catch (NumberFormatException e) { System.out.println(«Ошибка в аргументах. Использовать следует так: java Main <число1> <число2> … «); } } }

С помощью метода map перебираются все аргументы. Все они преобразовываются в целое число методом Integer.parseInt и добавляются в список (массив) elems. Затем с помощью метода свёртки списка foldRight вычисляется сумма элементов.

Быстрый старт со Scala для начинающих и не очень

Scala – строгий статически типизированный JVM-based язык, успешно совмещающий парадигмы объектно-ориентированного и функционального программирования. В языке есть классы, функции высшего порядка, анонимные функции, обобщенное программирование. Использование Java-кода из Scala не вызывает трудностей, синтаксически языки очень близки. В этой статье мы разберем основные элементы языка, достаточные для того, чтобы начать на нем писать.

Настройка окружения

Scala — язык, работающий на JVM, поэтому для работы требует установленную JDK (минимальная версия 1.6). Ее можно взять отсюда. После установки JDK можно приступить к установке самой Scala. Скачать свежую версию можно на официальном сайте. Последняя версия на момент написания статьи — 2.11.6.

Для того, чтобы все корректно работало из командной строки, рекомендуется прописать переменные среды JAVA_HOME и SCALA_HOME, а также дополнить переменную PATH путями к выполняемым файлам. На Linux и MacOS это делается так:

export JAVA_HOME=<путь к каталогу c установленной Java, в котором есть папка ‘bin’> export SCALA_HOME=<путь к каталогу c установленной Scala, в котором есть папка ‘bin’> export PATH=$PATH:$JAVA_HOME/bin:$SCALA_HOME/bin

Для того, чтобы сохранить эти настройки, их надо прописать в ~/.bashrc или ~/.bash_profile.

На Windows команда немного другая:

set JAVA_HOME=<путь к каталогу c установленной Java, в котором есть папка ‘bin’> set SCALA_HOME=<путь к каталогу c установленной Scala, в котором есть папка ‘bin’> set PATH=%PATH%;%JAVA_HOME%\bin;%SCALA_HOME%\bin

Прописать эти опции постоянно можно в настройках системы: Control Panel → Advanced System Settings → Environmental Variables.

После выполнения всех манипуляций можно проверить результат, запустив:

> java -version java version «1.8.0_31» Java(TM) SE Runtime Environment (build 1.8.0_31-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode) > scala -version Scala code runner version 2.11.6 — Copyright 2002-2013, LAMP/EPFL

SBT

Простые скрипты и маленькие программы можно, конечно, компилировать и запускать вручную с помощью команд scalac и scala. Однако, по мере того, как количество файлов будет расти, ручная компиляция будет становиться все более нудной. Вместо этого используют системы сборки. Для сборки кода на Scala можно использовать стандартные для Java (неофициально) maven, gradle или ant, но сообщество и сами разработчики рекомендуют sbt (simple build tool).

Примечание: если вы устанавливаете sbt, то можете пропустить отдельную установку scala, так как система сборки скачает ее автоматически

Описание процесса сборки находится либо в файле build.sbt в корне проекта, либо в файлах .scala в папке project там же. Само описание – это программа на Scala (которая, в свою очередь, может собираться с помощью sbt как отдельный проект, который… ну, вы поняли).

Синтаксис .sbt-файла напоминает синтаксис Scala с некоторыми дополнениями и ограничениями. Минимальный build.sbt выглядит примерно так (пустые строки обязательны):

name := «My Project» version := «0.1.0» scalaVersion := «2.11.6» libraryDependencies ++= Seq( «org.scalatest» %% «scalatest» % «1.6.1» % «test» )

Исходники помещаются в папку src/main/scala и src/test/scala по пути, соответствующем иерархии пакетов (как в Java). Чтобы собрать, протестировать и запустить проект, необходимо в любой поддиректории проекта выполнить следующие команды:

> sbt compile > sbt test > sbt run

или через интерактивную консоль:

> sbt sbt> compile sbt> test sbt> run

Последовательное выполнение команд выглядит немного необычно (обратите внимание на точку с запятой в начале — это особенность синтаксиса):

> sbt sbt> ; compile; test; run

REPL

Отличным помощником в разработке будет REPL (Read-Eval-Print-Loop), или по-другому, интерактивная консоль. Очень удобно проверять в ней небольшие функции, отлаживать код или просто посмотреть возможности языка. Для запуска REPL наберите sbt console в командной строке. Вы увидите примерно следующее:

> sbt console Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0 Set current project to test (in build file:/C:/Users/foxmk/Desktop/test) Starting scala interpreter… Welcome to Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_25). Type in expressions to have them evaluated. Type :help for more information. scala>

Все! Можно писать команды на Scala и сразу же их выполнять:

scala> 1 + 1 res1: Int = 2 scala> def f(x: Int) = x * 2 f: (x: Int)Int scala> f(4) res2: Int = 8 scala> println(«Hello!») Hello! scala>

Для выхода из REPL можно нажать Ctrl+D. Все примеры на Scala далее можно протестировать в REPL, для вставки больших кусков кода можно воспользоваться командой :paste.

IDE

Использование IDE для разработки на Scala не обязательно, однако сильно упрощает процесс. Скала — язык со сложной семантикой, поэтому возможности IDE более ограничены, чем, скажем, при разработке на Java. Тем не менее даже простая подсветка несуществующих методов и автодополнение существующих может сильно облегчить жизнь. Самые популярные IDE для Scala — это IntelliJ IDEA и Eclipse. Для IDEA есть плагин от JetBrains, в случае с Eclipse есть ее вариант Scala IDE.

Переменные, значения и типы.

В Scala переменные и значения объявляются ключевым словом val или var. val — это неизменяемая переменная (значение), аналог final в Java. var — обычная переменная. Например:

> val x = «Some immutable string» > var y = «I CAN CHANGE THIS!» > y = «WHOA! I HAVE CHANGED THIS!» > x = «OOPS!» Error:(3, 4) reassignment to val x = «OOPS!»};} ^

Аналогичный код на Java будет выглядеть так:

public final String x = «Some immutable string»; public String y = «I CAN CHANGE THIS!»; y = «WHOA! I HAVE CHANGED THIS!»; x = «OOPS!»; // Ошибка компиляции

Здесь мы видим сразу несколько приятных особенностей Scala:

  1. точка с запятой не обязательна (работает автоматический вывод);
  2. указания типа переменной необязательно (также работает автоматический вывод типов);
  3. ключевое слово public подразумевается по умолчанию.

Типы переменных указываются после имени, через двоеточие. Также в Scala нет, как таковых, примитивных типов (int, float, boolean и т.д.). Их заменяют соответствующие классы Int, Float, Boolean и т.д. Любая переменная — экземпляр какого-либо класса. Иерархия классов начинается с Any, все классы наследуются от него (аналог Object в Java).

val i: Int = 0 // Scala public int i = 0; // Java val flag: Boolean = true // Scala public boolean flag = true; // Java

Применение привычных операторов, при этом, на самом деле — вызов метода: a + b тождественно a.+(b). Вариант записи без точки применим к любым методам (с некоторыми ограничениями).

Функции, анонимные функции, методы

Функция в Scala объявляется с помощью ключевого слова def. Пример объявления и применения функции:

def addOne(a: Int) = a + 1 addOne(1) // res0: Int = 2

Аналогичный код на Java:

public int addOne(int a) { return a + 1; }

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

На самом деле, функция — это тоже объект. Каждая функция в Scala — это экземпляр класса Function, у которого есть метод apply. Поэтому мы вполне можем записать так (знак подчеркивания ставится на место аргумента функции):

val functionValue = addOne _ functionValue.apply(1) // res1: Int = 2

Вызов метода apply подразумевается по умолчанию, поэтому использование функций внешне выглядит как в Java:

functionValue(1) // res2: Int = 2

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

Конечно, присутствуют анонимные функции (лямбда-функции). Они объявляются так:

val f = (x: Int) => x + 1 f(1) // res3: Int = 2

Здесь мы объявляем анонимную функцию, которая принимает один целочисленный аргумент и присвоил ее переменной f, после чего применяем f как обычную функцию.

Классы и объекты

Если вы программировали на Java, многие вещи, касающиеся объектно-ориентированного программирования будут вам знакомы. Класс объявляется ключевым словом class, новый экземпляр — через new. Методы класса — это функции, объявленные в его теле. Поля класса указываются сразу после имени, как список аргументов. При этом, по умолчанию они объявляются как private val. То есть, если мы не укажем никаких модификаторов, указанное поле будет доступно только внутри класса и будет неизменяемым. Класс можно сделать абстрактным, добавив abstract перед объявлением. Основное отличие от Java здесь заключается в отсутствии конструктора. Код, который должен выполняться при создании объекта, пишется прямо в теле класса. Как при этом реализовать несколько конструкторов с различным числом аргументов, мы рассмотрим позже. Пример использования класса:

class Foo(x: Int) { def bar(y: Int) = x + y } val foo = new Foo(1) // foo: Foo = Foo@69007238 foo.bar(1) // res4: Int = 2

И аналогичный код на Java:

class Foo { private int x_; Foo(int x) { _x = x; } public int bar(int y) { return x + y; } } Foo foo = new Foo(1) int result = foo.bar(1) // result теперь равен 2

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

Кроме того, в Scala мы можем объявить сразу объект, без создания класса, с помощью ключевого слова object. Таким образом реализуется паттерн «Одиночка» (Singleton).

object Singleton(field: String) { def squareMe(x: Int) = x*x } Singleton.squareMe(2) // res5: Int = 4

Аналог на Java будет куда более многословен.

public class Singleton { private static Singleton instance = null; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } public int squareMe(int x) { return x*x; } } int a = Singleton.getInstance().squareMe(2) // a теперь равен 4

В этом примере мы пометили конструктор как protected, чтобы исключить возможность его вызова извне, обращение к объекту будет осуществляться через метод getInstance(), который при первом своем вызове инициализирует экземпляр класс, а при последующих возвращает уже созданный экземпляр. Кроме того, вполне допустимо существование объекта и класса с одним и тем же именем, при этом они делят область видимости. Поэтому необходимость в директиве static отпадает — методы, объявленные не в классе, а в объекте ведут себя как статические. Такой объект называется в терминологии Scala «companion object» («объект-компаньон»).

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

Вариант Scala:

object MyUselessClass { def staticMethod(x: Int) = x + 5 def apply(immutableField: Int): MyUselessClass = new MyUselessClass(immutableField, 2) def apply(immutableField: Int, mutableField: Int): MyUselessClass = new MyUselessClass(immutableField, mutableField) def apply(immutableField: Int, mutableField: Int, privateField: Int): MyUselessClass = new MyUselessClass(immutableField, mutableField, privateField) } class MyUselessClass(val immutableField: Int, var mutableField: Int, privateField: Int = 8 /*значние по умолчанию*/) { def instanceMethod() = { val sumOfFields = immutableField + mutableField + privateField MyUselessClass.staticMethod(sumOfFields) } } // Первый конструктор, обратите внимание на отсутствие ‘new’, // так как это на самом деле вызов метода ‘apply’ val myUselessObject = MyUselessClass(1) // аналогично предыдущему варианту val myAnotherUselessObject = MyUselessClass.apply(1) // Третий конструктор val myThirdUselessObject = MyUselessClass(1, 2, 3) // Вызов метода myUselessObject.instanceMethod() // res6: Int = 16 // Поля доступны также, как методы myUselessObject.mutableField // res7: Int = 2 myUselessObject.immutableField // res8: Int = 1 myUselessObject.mutableField = 9 myUselessObject.mutableField // res9: Int = 9 // Вызов статического метода MyUselessClass.staticMethod(3) // res10: Int = 8

Вариант Java:

public class MyUselessClass { private int immutableField_; private int mutableField_; private int privateField_ = 8; MyUselessClass(int immutableField) { immutableField_ = immutableField; mutableField_ = 2; } MyUselessClass(int immutableField, int mutableField) { immutableField_ = immutableField; mutableField_ = mutableField; } MyUselessClass(int immutableField, int immutableField, int privateField) { immutableField_ = immutableField; mutableField_ = mutableField; privateField_ = privateField; } int getImmutableField() { return immutableField; } int getMutableField() { return mutableField; } void setMutableField(int newValue) { mutableField = newValue; } public static int staticMethod(int x) { return x + 5; } public int instanceMethod() { int sumOfFields = immutableField + mutableField + privateField; return staticMethod(sumOfFields); } } // Первый конструктор MyUselessClass myUselessObject = new MyUselessClass(1) // Третий конструктор MyUselessClass myAnotherUselessObject = new MyUselessClass(1, 2, 3) // Вызов метода myUselessObject.instanceMethod() // вернет 16 // Поля доступны также, как методы myUselessObject.getMutableField // вернет 2 myUselessObject.getImmutableField // вернет 1 myUselessObject.setMutableField(9) myUselessObject.getMmutableField // вернет 9 // Вызов статического метода MyUselessClass.staticMethod(3) // вернет 8

Интерефейсы и трейты

Аналогом Java-интерфейса в Scala является трейт (trait). Как ни удивительно, объявляется он с помощью ключевого слова trait. Как и интерфейсы Java, трейты содержат только объявления методов и допускают множественное наследование. В отличие от интерфейса, в трейте можно описывать поля класса и частично реализовывать методы. Наследование как трейтов, так и абстрактных классов осуществляется с помощью extend (первый родитель) и with (последующие родители). Пример использования:

trait FirstTrait { def foo(x: Int) } trait SecondTrait { def bar(y: Int) = y + 5 } class ComplexClass extends FirstTrait with SecondTrait { override def foo(x: Int) = x * 2 }

Ключевое слово override необязательно, но его использование является хорошей практикой.

Другие особенности и отличия от Java

Как и в Java, в Scala классы, трейты и функции можно параметризовать. Параметры типов пишутся в квадратных скобках после имени класса или функции. Так определяется интерфейс Foo, который принимает некоторый тип A и содержит метод bar? который принимает значение типа A и некоторого типа B и возвращает объект типа C. Конкретные типы A, B и C будут определены в реализации интерфейса.

trait Foo { def bar(a: A, b: B): C // метод, который принимает аргументы типа A и B и возвращает тип C }

Конструкция if/else всегда возвращает значение выражения, которое стоит последним ввыполняемом блоке. Скобки вокруг условия обязательны, скобки вокруг тела, в котором только одна инструкция, можно опустить.

val a = if (true == false) { // true-branch } else { // false-branch }

Блок try/catch/finally выглядит в Scala так:

try { // some code } catch { case e: SomeException => ??? case e: SomeOtherException => ??? } finally { // some code }

Циклы while ничем не отличаются от варианта в Java:

var i = 1000 while (i > 0) { i -= 1 }

А циклы for — наоборот, совсем не похожи (о них мы подробнее поговорим в следующей статье):

val numbers = 1 to 1000 for (number <- numbers) { print(number) }

Также вы, возможно, могли заметить литерал ???. Он имеет тип Nothing который является подкласс любого класса. При вызове ??? кидает исключение NotImplemented. Это примерно аналог undefined в Python. ??? можно ставить в качестве заглушки вместо тела функции, к написанию которого вы планируете вернуться позже.

Итак, мы установили и настроили среду разработки для Scala, посмотрели основные элементы языка, сходства с Java и отличия от нее. Этого вполне должно хватить для того, чтобы начать писать простой и рабочий код. В следующей статье мы подробнее рассмотрим элементы функционального программирования, case-классы, pattern-matching и другие высокоуровневые особенности языка.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *