Часть 1. Базовые типы Scala и pattern matching

Страничка курса: https://maxcom.github.io/scala-course-2018/

План

  1. Материалы курса, обратная связь
  2. Обзор курса
  3. Средства разработки и первые шаги
  4. Case-классы
  5. Алгебраические типы.
  6. Pattern matching

Материалы курса и обратная связь

Слайды, примеры программ и другие файлы доступны в репозитарии https://github.com/maxcom/scala-course-2018. Содержимое будет пополняться по ходу курса.

Для обсуждений и вопросов по курсу используем telegram чат @scalacourse2018 (доступ по ссылке).

Курс читает Максим Валянский

окончил ВМиК в 2001 году

занимаюсь разработкой ПО сколько себя помню

работаю архитектором в компании Solar Security

Кто мы такие и почему Scala?

Мы разрабочики из компании Solar Security.

Компания занимается:

  • Разработка продуктов, решающих задачи ИБ в компаниях.
  • Аутсорсинг ИБ

Мы занимаемся "Дозором" – инструментом контроля коммуникаций сотрудников, выявления ранних признаков корпоративного мошенничества и проведения расследований.

https://solarsecurity.ru/products/solar_dozor/

Продукту более 18 лет.

Первые версии написаны на Scheme – функциональном динамическом языке, диалекте Lisp.

В ходе эволюции у нас появилось много компонентов на Java.

Java это отличный runtime, opensource community, библиотеки для интеграции со сторонним ПО.

Но сейчас мы отказались и от Scheme,
и от языка Java в "Дозоре".

(хотя у нас еще много Java кода)

Scheme (mzscheme, racket) сейчас это:

  • Плохой runtime – медленный, много ошибок, проблемы с многопоточностью
  • Мало готовых библиотек, плохое качество
  • Плохие средства отладки и диагностики
  • Сложности с поиском программистов

Но мы все еще любим функциональное программирование*

* хотя каждый разработчик по своему понимает что такое ФП

Мы верим что функциональной программирование сделает наш софт качественнее, а разработку – более быстрой и предсказуемой.

Что не так в Java?

  • Изменяемое состояние и побочные эффекты везде
  • Простые вещи делаются сложно - много избыточного кода
  • Сложные вещи делаются еще сложнее - вспомним Spring Framework и J2EE
  • Не достаточно строгая система типов
  • ...

Мы выбрали JVM как платформу и остановились на двух языках – Scala и Clojure.

Первый Scala компонент у нас появился осенью 2013 года в качестве эксперимента.

Это была распределенная система хранения бинарных данных - "файловое хранилище".

По результатам вывода в production эксперимент посчитали удачным.

Сейчас довольно большая часть продукта написана на Scala, и у нас много планов по развитию.

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

Работающий на JVM и хорошо интегрирующийся с существующим Java кодом

обо всём языке говорить долго

хотя есть что рассказать

рассмотрим базовые возможности языка
и перейдем к практике

Состав курса

  1. Введение в программирование на Scala
  2. Потоки и асинхронное программирование.
  3. Разработка и использование веб-сервисов

После каждого занятия будут предложены задачки по теме и одно большое практическое задание на весь курс.

Средства разработки

Есть много вариантов, мы будем использовать
Intellij IDEA Community Edition + Scala plugin

https://www.jetbrains.com/idea/download/

  1. Ставим IDEA, запускаем
  2. Добавляем Scala plugin (configure -> plugins)
  3. Создаем новый проект Scala
  4. Выбираем sbt; версии самые новые
    (Scala 2.12.4; sbt 1.1.0)

Hello, World!


object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}
					
Структура проекта (sbt):

build.sbt       -- настройки сборки
project         -- еще настройки сборки
project/target  -- кеш компилятора и вспомогательные файлы
src             -- исходные файлы и ресурсы
src/main        -- основной код
src/main/scala  -- основной код на Scala
src/test        -- исходные файлы и ресурсы тестов
src/test/scala  -- код тестов на Scala
target          -- результат компиляции
					
Интерактивная разработка: Scala Worksheet

Переменные и функции

можно присвоить имя результату выражения:

val something = 10 // константа
var something: Int = 10 // переменная

тип выводится автоматически

В Scala почти всё является выражением:


val ageGroup = if (age >= 18) "grownup" else "minor"
					

val result: Int = {
  val one = 1
  val two = 2
  
  one + two // "возвращается" последнее значение в блоке
}
					
объявление функции:
def square(x: Int) = x * x
функция - это значение:
val f: (Int ⇒ Int) = square
(её тип - Function1[Int, Int])

Пара (tuple)

Комбинация из двух значений:

val pair: (Int, Int) = (1, 2)
// Tuple2[Int, Int]
					

деконструкция


val (first, second) = pair
					

бывают еще "тройки" и более - до 22

Немного о коллекциях

Подробно устройство коллекций рассмотрим на втором занятии.

Seq[T] – общий тип для коллекций, имеющих определенный порядок (списки, массивы, вектора и т.п.)

ArrayBuffer[T] – аналог ArrayList из Java

Vector[T] – неизменяемый аналог ArrayList


val buffer = ArrayBuffer[Int](1, 2, 3)

buffer += 4 // добавление элемента

buffer(1) // получение элемента

// buffer еще и функция
val f: (Int ⇒ Int) = buffer
					

У стандартных коллекций много полезных функций

Справка на Vector: scaladoc.

Пример: разделение Vector на две части

val v = Vector(1, 2, 3, 4)

val (first, second) = v.splitAt(v.length / 2)

// first == Vector(1,2)
// second == Vector(3,4)
					

Case-классы

Механизм для создания собственных типов для данных.


case class Address(`type`: String, value: String) {
  def toStringAddress = s"${`type`}:$value"
}
					

Это не ООП! Данные не изменяемые, обычно не содержат бизнес-логики.

Собственный тип лучше, чем просто значения:

Address(`type`, value)

против пары

(String, String)

Собственный тип можно заводить и для простых значений, например:

case class UserId(uuid: UUID)
case class GroupId(uuid: UUID)

типы позволяют не путать значения между собой

Что есть в case class?

  • геттеры для полей
  • toString
  • equals и hashcode
  • создание без "new"
  • экстрактор
  • сериализация, product и еще...

Экстракторы

Можно разобрать класс обратно:


val address = Address("email", "abuse@sportloto.ru")

val Address(_, email) = address
					
Статическая фабрика и экстрактор:

object Address {
  def apply(`type`: String, value: String) = 
    new Address(`type`, value)
  
  def unapply(address: Address): Option[(String, String)] =
    Some((address.`type`, address.value))
}
					
* объект-компаньон, заменяет static декларации Java * для case классов apply/unapply создаются автоматически

Алгебраические типы

Case классы можно объединить в иерархию


sealed trait Expr

case class Number(value: Int) extends Expr
case class Plus(lhs: Expr, rhs: Expr) extends Expr
case class Minus(lhs: Expr, rhs: Expr) extends Expr
					
* дальше будет более правильная версия этого примера

Pattern matching

def value(expression: Expr): Int = expression match {
  case Number(value)   ⇒ value
  case Plus(lhs, rhs)  ⇒ value(lhs) + value(rhs)
  case Minus(lhs, rhs) ⇒ value(lhs) - value(rhs)
}				

val result = value(Plus(Number(2), Number(2)))
					

Pattern matching - альтернатива полиморфизму на методах:

trait Expr {
  def eval: Int
}
case class Number(value: Int) extends Expr {
  override val eval = value
}
case class Plus(lhs: Expr, rhs: Expr) extends Expr {
  override def eval = lhs.eval + rhs.eval
}
case class Minus(lhs: Expr, rhs: Expr) extends Expr {
  override def eval = lhs.eval - rhs.eval
}
Plus(Number(2), Number(2)).eval

Две модели:

  1. Фиксированная "схема" данных, произвольные операции – PM
  2. Фиксированные операции, большое разнообразие объектов – ООП

Более правильная версия примера


sealed trait Expr extends Product with Serializable

final case class Number(value: Int) extends Expr
final case class Plus(lhs: Expr, rhs: Expr) extends Expr
final case class Minus(lhs: Expr, rhs: Expr) extends Expr
					
почему "extend Product with Serializeable"?

val number: Expr = Number(3)
val expr = Plus(Number(2), Number(2))
val buffer = ArrayBuffer(Number(1), expr)

// не компилируется
buffer += number
					

потому что тип buffer вот такой:


ArrayBuffer[Product with Serializable with Expr]
					
почему "final case class"?

запрещаем наследование

помогаем компилятору

Option[T]

Тип с двумя вариантами:
  • Some[T] - контейнер для одного значения
  • None - значение отсутствует

более безопасная замена null


val v = Vector(1, 2, 3, 4, 5)

val r: Option[Int] = v.find(x ⇒ x > 2)

// r = Some(3)				
					

плохой вариант работы с Option:


if (r.isDefined) {
  println(r.get) // бросает исключение если значения нет
}					
					
несколько правильных вариантов:

// 1
r match {
  case Some(k) ⇒ println(k)
  case None    ⇒ println("None")  
}

// 
// 2
println(r.getOrElse("None"))
					
еще варианты рассмотрим на 3-м занятии

Try[T]

Обработка исключений в Scala похожа на Java:

try {
  1 / 0
} catch {
  case ex: ArithmeticException ⇒
    println(ex.getMessage)
}
					

try - тоже выражение, возвращает последнее значение

Иногда мы не хотим обрабатывать ошибки прямо сейчас:
  • хотим положить в список и потом обработать
  • передать между потоками
  • ...

Тип Try

  • Success - содержит значение
  • Failure - содержит исключение

получим его так:

val result: Try[Int] = Try {
  1 / 0
}					

import scala.util.{Random, Try}

// fill - функция с двумя блоками параметров
// fill[A](n: Int)(elem: => A)
val vector = Vector.fill(10) {
  Try {
    1 / Random.nextInt(5)
  }
}

vector.count(x ⇒ x.isSuccess)				
					

Могут ли функции возвращать Try?

Да, но это "антипаттерн".

Either[A, B]

Выбор из двух значений:
  • Right(x) - "правильное" значение
  • Left(y) - "левое" значение

Домашнее задание

Реализуем сортировку слиянием (merge sort)
для Vector[Int]

Описание алгоритма: на wikipedia

Неплохая визуализация
(надо выбрать "merge sort")

Deadline - утро среды следующей недели.

Простой вариант - m.valyanskiy@solarsecurity.ru.

Присылайте только исходники; код должен работать!

"Сложный" вариант - используем gitlab.com:

  • Зарегистрируйтесь на gitlab.com (это бесплатно)
  • Заводите приватный репозиторий (свой под каждое задание)
  • Добавляйте мне доступ - @maximvalyanskiy
  • Киньте ссылку на gitlab мне на почту

Unit-тесты*

дополнительная часть, если успеем

Напишем несколько тестов для функции сортировки.

Используем фреймворк Specs2

Зачем нужны тесты?

Убедиться, что функция работает.

"Подстраховать" будущие изменения.

Тесты кладут в специальную папку "test".

Они не попадают в production код.

Unit-тесты запускаются в сборочном окружении.

Библиотеки могут создавать синтаксис, не похожий на обычный код.

Например можно вызывать метод без "."


val v = Vector.fill(10)(Random.nextInt(20) - 10)
def positive(i: Int) = i > 0

val (pos, neg) = v.partition(positive)

val (pos2, neg2) = v partition positive	
					

И еще есть другая "магия".

Тесты на Specs2 выглядят вот так:


import org.specs2.mutable.Specification

class SorterTest extends Specification {
  "sort function" should {
    "preserve vector length" in {
      val vector = Vector(5, 10, 343, 43, 1)

      Sorter.sort(vector) must have size vector.length
    }
  }
}
					

Как запустить тест?

В консоли "sbt test"

В IDEA: в выпадающем меню у класса с тестом - "Run".

В библиотеке много готовых условий для коллекций:


Seq(1, 2, 3) must beSorted
Seq(1, 2, 3) must contain(2)
Seq(2, 4, 1) must containTheSameElementsAs(Seq(1, 4, 2))
					

(читайте документацию)

Вместе с заданием пишем тесты.

Заготовка - тут: unit-test-demo.