Страничка курса: https://maxcom.github.io/scala-course-2022/
Максим Валянский
окончил ВМиК МГУ в 2001 году;
занимаюсь разработкой ПО сколько себя помню;
веду курс по Scala уже 3-й раз;
работаю архитектором в компании
«Ростелеком-Солар».
Слайды, примеры программ и другие файлы доступны в репозитарии https://github.com/maxcom/scala-course-2022. Содержимое будет пополняться по ходу курса.
До Scala в продукте были:
Scheme (Lisp), OCaml,
Java, Python, C++ и др.
Хотелось:
Мы верим что функциональной программирование сделает наш софт качественнее, а разработку – более быстрой и предсказуемой.
Первый Scala компонент у нас появился осенью 2013 года в качестве эксперимента.
Это была распределенная система хранения бинарных данных - "файловое хранилище".
По результатам вывода в production эксперимент посчитали удачным.
Сейчас довольно большая часть продукта написана на Scala, и у нас много планов по развитию.
Scala — мультипарадигмальный язык программирования, спроектированный кратким и типобезопасным для простого и быстрого создания компонентного программного обеспечения, сочетающий возможности функционального и объектно-ориентированного программирования.
Работающий на JVM и хорошо интегрирующийся с существующим Java кодом
обо всём языке говорить долго
рассмотрим базовые возможности языка
и перейдем к практике
Видео: Ставим JDK и Intellij IDEA
build.sbt -- настройки сборки
project -- еще настройки сборки
project/target -- кеш компилятора и вспомогательные файлы
src -- исходные файлы и ресурсы
src/main -- основной код
src/main/scala -- основной код на Scala
src/test -- исходные файлы и ресурсы тестов
src/test/scala -- код тестов на Scala
target -- результат компиляции
Синтаксис Scala: смесь Си + ML.
"Better Java", C, Python, ...
справочник по языку
или google + stackoverflow
object HelloWorld extends App {
println("Hello, world!")
}
// класс-синглтон для JVM
// наследование от App
object HelloWorld extends App {
// код инициализации класса
println("Hello, world!")
}
Блоки вместо выражений
object HelloWorld extends App {
println({
"Hello, world!"
})
}
object HelloWorld extends App {
println({
var str = "Hello, " // переменная
val add = "world!" // константа
// val короче чем "final int" или "const int"
str += add
str // "возвращается" последнее значение в блоке
})
}
Иммутательность "по-умолчанию":
объявляем функцию
object HelloWorld extends App {
println({
// функцию можно объявить где угодно
def square(x: Int) = x * x
square(10)
})
}
def square(x: Int) = x * x
функция - это значение:
val f: (Int => Int) = square
(её тип - Function1[Int, Int])
if тоже является выражением:
def abs(x: Int) = {
if (x >= 0) {
x
} else {
-x
} // и никакого "return"!
}
return не используем
Типы выводятся в:
(кроме рекурсивных)
Рекомендуется указывать типы:
while
var a: Int = 0
while (a<10) {
a += 1
println(a)
} // бывает еще do { ... } while
break/continue нет, но если очень хочется то есть
util.control.Breaks
for - не цикл, но "прикидывается" им:
for (i <- 0 to 10) {
println(i)
}
// про коллекции будет отдельная лекция
for (i <- Vector(1, 2, 3)) {
println(i)
}
for .. yield - "List comprehension" из Python
println(for (v <- 1 until 10) yield v * v)
// аналог на Python:
// print([i * i for i in range(1, 10)])
с условием
for { // фигурные скобки дают более сложный синтаксис
v <- 1 until 10 if v%2 == 0
} yield {
v * v
}
// Python:
// [i*i for i in range(1, 10) if i%2==0]
писать на Scala - не сложно
val pair: (Int, Int) = (1, 2)
val pair2 = "key" -> "value"
// Tuple2[Int, Int]
возврат нескольких значений из функций
преобразование коллекций
Деконструкция
плохой способ:
val first = pair._1
val second = pair._2
val (first, second) = pair
// ввели константы "first" и "second"
бывают еще "тройки" и более - до 22
ООП похоже на Java
Свой тип для данных.
case class Address(`type`: String, value: String) {
def toStringAddress = s"${`type`}:$value"
}
Это не ООП! Данные не изменяемые, обычно не содержат бизнес-логики.
Собственный тип лучше, чем просто значения:
case class Address(`type`: String, value: String)
val address = Address("email", "someone@gmail.com")
// type - ключевое слово, по этому в апострофах
против пары
val address: (String, String) = ("email", "someone@gmail.com")
Собственный тип можно заводить и для простых значений
// где тут user, а где group?
userService.addUserToGroup(uuid1, uuid2)
case class UserId(uuid: UUID)
case class GroupId(uuid: UUID)
типы позволяют не путать значения между собой
(но это не всегда эффективно)
Создание без new
у обычных классов identity,
тут только содержимое
// объект компаньон класса Address
object Address {
// apply генерируется автоматически, можно переопределить
// например отдавать значения из кеша
def apply(`type`: String, value: String) =
new Address(`type`, value)
}
* объект-компаньон заменяет static декларации Java
Что есть в case class?
val address = Address("email", "abuse@sportloto.ru")
val value = address.value
Экстрактор
// похоже на деконструкцию пары
// ввели новую константу - "email"
val Address(_, email) = address
// "дырка" позволяет пропустить ненужное объявление
можно переопределить - функция 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
* дальше будет более правильная версия этого примера
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)))
match перебирает экстракторы
(экстрактор может не подойти)
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
Две модели:
Сделаем более правильное определение ADT
val number: Expr = Number(3)
val expr = Plus(Number(2), Number(2))
val buffer = ArrayBuffer(Number(1), expr)
// не компилируется
buffer += number
ArrayBuffer[Product with Serializable with Expr]
а изменяемые коллекции - инвариантные
Более правильная версия примера
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
// дополнительно запретили наследование
// sealed запрещает только прямых потомков
более безопасная замена null
val v = Vector(1, 2, 3, 4, 5)
val r: Option[Int] = v.find(x => x > 2)
// r = Some(3)
val rr: Option[Int] = v.get(0)
// rr = Some(1)
if (r.isDefined) {
println(r.get) // бросает исключение если значения нет
}
// компилятор не верифицирует r.get
// 1
r match {
case Some(k) => println(k)
case None => println("None")
}
//
// 2
println(r.getOrElse("None"))
еще варианты рассмотрим на 3-м занятии
Option заменяет null,
но null в JVM нельзя избежать
val opt1: Option[String] = null // упс
val opt2: Option[String] = Some(null) // упс
val opt3: Option[String] = Option(null) // правильно
try {
1 / 0
} catch {
// это pattern matching по типу
case ex: ArithmeticException ⇒
println(ex.getMessage)
1
}
try - тоже выражение, возвращает последнее значение
Тип Try - ADT
получим его так:
val result: Try[Int] = Try {
1 / 0
}
import scala.util.{Random, Try}
val vector = Vector.fill(10) {
Try {
1 / Random.nextInt(5)
}
}
// число успешных значений
vector.count(x => x.isSuccess)
// fill - функция с двумя блоками параметров
// fill[A](n: Int)(elem: => A)
// "elem: => A" будет на лекции про ленивые вычисления
Могут ли функции возвращать Try?
да, но это делает все сложнее.
функция может и упасть, и вернуть Failure
Left может быть не только исключением
Left может быть конкретным исключением
Напоминаю: