Страничка курса: https://maxcom.github.io/scala-course-2022/
Реализация Seq по-умолчанию - List
Посмотрите на исследование производительности коллекций Scala: Benchmarking Scala Collections.
(исследование старое,
Vector сейчас лучше чем там измерено)
Vector - современная персистентная коллекция,
без этих недостатков.
Используется и в Scala, и в Clojure
Реализация до Scala 2.13.2 на базе аналогичной структуры Clojure
В 2.13.2 и далее - новая реализация
Расскажу о "классической" реализации
так как она проще
До 32 элементов - массив
До 1024 элементов (32*32)
Очередной уровень
Добавление элемента - два уровня
линии показывают переиспользование элементов
Добавление элемента - три уровня
(на картинке заполнены все листы исходного дерева)
Добавление в начало - аналогично;
Vector хранит смещение первого элемента
Добавление в хвост и в голову одинаково эффективно
Стоимость операций - effectively constant:
Почему effectively constant?
Максимум 6 уровней, это достаточно
Vector - не List:
реализация - префиксное дерево с 32 ветвями,
похоже на "разряженный" Vector
для пользователя:
Map("one" -> "first", "two" -> "second", "three" -> "third")
m.get("one") // Some("first")
val m1 = m + ("five" -> "fifth") // добавление
val m2 = m - "one" // удаление
map/flatMap/filter/fold
аналогично
Seq[(K,V)]
m.map(p => p._1.toUpperCase -> p._2)
операции не меняют тип исходной коллекции
если результат не пара то результат будет Seq
Map - еще и частично определенная функция:
val m = Map("one" -> 1, "two" -> 2)
// определена там где есть данные по ключу
List("one", "two", "three").collect(m)
// эквивалентно
List("one", "two", "three").flatMap(m.get)
// List(1, 2)
Ключ - неизменяемый объект любого типа
Метод hashCode возвращает Int для любого объекта
Добавление и удаление -
effectively constant, как у Vector.
Поиск - effectively constant,
если хеш-функция хорошая.
Значения с одинаковым
хеш-кодом хранятся в списке.
Временное представление коллекции для цепочек преобразований.
В Scala 2.12 и ранее были проблемы.
case class Person(id: Int, name: String, student: Boolean)
def makeIndex(persons: Vector[Person]): Map[Int, Person] = {
// каждая операция - новая структура
persons.filter(_.student).map(p => p.id -> p).toMap
}
case class Person(id: Int, name: String, student: Boolean)
def makeIndex(persons: Vector[Person]): Map[Int, Person] = {
// похожи на "stream" из Java
persons.view.filter(_.student).map(p => p.id -> p).toMap
}
вычисление происходит в "toMap"
Два вида View для коллекций:
SeqView
IndexedSeqView
MapView для Map
View - не коллекция
Откладываем вычисления до момента,
когда нужен результат
(View - это тоже ленивые вычисления)
Параметры функции могут вычисляться:
Пример: Option.getOrElse
// реализация из Scala 2.13.1
@inline final def getOrElse[B >: A](default: => B): B =
if (isEmpty) default else this.get
// пример
Option(v).getOrElse(throw new RuntimeException("Oops!"))
Значение вычисляется заново каждый раз
// метод List[A]
def fill[A](n: Int)(elem: => A): List[A]
создает новый список с разными элементами
List.fill(10)(Random.nextInt)
Такие вызовы похожи на передачу функции без аргументов
Библиотеки могут таким образом
расширять "синтаксис".
Например, Try вместо try, или свой цикл (List.fill) и т.п.
В Java часто нужны аннотации + фреймворки
(например @Transactional)
"Ленивые" значения - вычисляются один раз, результат сохраняется (memoization)
import java.time.{Duration, Instant}
lazy val lazyCurrent = Instant.now
val current = Instant.now
Thread.sleep(1000)
Duration.between(lazyCurrent, current)
// разница больше секунды
при отладке помним об "эффекте наблюдателя".
lazy работает и в классах, и внутри функций
(но помним про overhead,
особенно у глобальных lazy val)
Превращаем call by name в lazy:
def repeat(n: Int, v: => Int) {
// вычисляется 0 раз если n = 0, 1 раз в остальных случаях
lazy val cached = v
List.fill(n)(cached)
}
Еще пример - регистронезависимый id,
хотим сохранить исходное написание
final case class UserId(id: String) {
private lazy val loId: String = id.toLowerCase()
override def equals(obj: Any) = {
obj match {
case other: UserId ⇒
other.loId == loId
case _ ⇒
false
}
}
override def hashCode() = loId.hashCode
}
демо-код, с некоторыми "локалями" будут проблемы
Stream - старая реализация из 2.12 и ранее
LazyList - 2.13+, исправлены некоторые недостатки
Структура похожа на List
val s: LazyList[Int] = 3 #:: 2 #:: 1 #:: LazyList.empty
У Stream два вида ячеек:
У LazyList аналогично, но реализация скрыта
(еще отличия будут дальше).
Cons ячейка вычисляет "хвост" при обращении
и сохраняет его. Только до следующего звена.
Функции тоже работают лениво, например map:
var n: Int = 0
val s: LazyList[Int] = LazyList.fill(100000) {
// побочны эффекты и ленивые вычисления лучше не совмещать
// это код только для демонстрации работы
n += 1
Random.nextInt
}
println(n) // 0
s.map(_ * 2).take(1).toVector
println(n) // 1
Пример реализации map:
def map(s: Stream[Int], f: Int ⇒ Int): Stream[Int] = {
if (s.isEmpty) {
s
} else {
f(s.head) #:: map(s.tail, f)
}
}
для LazyList чуть сложнее
Функции, обходящие весь список, "форсируют" его.
Например, length или foldLeft.
Stream может быть бесконечным
Фибоначчи: каждое последующее число равно сумме двух предыдущих чисел
import scala.math.BigInt
val fibs: LazyList[BigInt] =
BigInt(0) #::
BigInt(1) #:: // zip из двух списков делаем список пар
fibs.zip(fibs.tail).map { n =>
n._1 + n._2
} // список объявлен "рекурсивно"
fibs.take(5).toVector
Отличие Stream и LazyList:
Stream - неудачное название
Stream: первый элемент всегда вычислен,
LazyList - полностью ленивый.
Пример: решение Судоку.
Пример: поиск кратчайшего решения "лабиринта".
Минусы:
«Монада — всего лишь моноид из категории эндофункторов, что может быть проще?»
(c) A Brief, Incomplete, and Mostly Wrong History of Programming Languages
Монада это:
Много типов из разных областей являются монадами.
Монада - значение, помещенное в контекст.
Операции:
Рассмотрим на примере Option
def findUserId(name: String): Option[Int] = ???
def loadUserById(id: Int): Option[User] = ???
val opt = Option("username") // создание
opt.flatMap(findUserId).flatMap(loadUserById)
Последовательное вычисление,
пока не встретится None
for в Scala – не цикл
for { ... } yield { ... }
Комбинирует flatMap и map
(и еще filter, но это не для монад)
for без yield использует
foreach вместо последнего map
val jobTitle: Option[String] = for {
name <- opt // первая операция определяет тип
id <- findUserId(name)
user <- loadUserById(id)
} yield {
user.jobTitle
}
Посмотрите "desugar for" в IDEA
opt.flatMap(name =>
findUserId(name).flatMap(id =>
loadUserById(id).map(user =>
user.jobTitle)))
opt match {
case Some(name) ⇒
findUserId(name) match {
case Some(id) ⇒
loadUserById(id) match {
case Some(user) ⇒ user.jobTitle
case None ⇒ None
}
case None ⇒
None
}
case None ⇒
None
}
Монада - абстракция цепочки связанных вычислений.
Монада контролирует выполнение этой цепочки.
Законы, которые должны выполнять монады
"Left Identity"
Для любой монадической функции f
pure(x).flatMap(f) == f(x)
применение функции к значению в монаде эквивалентно применению функции к значению
"Right Identity"
m.flatMap(pure) == m
применение функции создания не меняет монаду
ассоциативность
m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))
уравнивает разные способы комбинации функций
Законы не всегда выполняются даже у реализаций из Scala-библиотеки
Try - тоже монада
Вычисляется пока не возникнет исключение
Either - монада.
Вычисляется правая сторона,
левая сторона - остановка вычисления.
Вычисления не обязательно должны происходить прямо сейчас и в текущем потоке.
Future - монада, выполняющая вычисление в другом потоке.
Рассмотрим её устройство на 5-й лекции.
Напоминаю: