Страничка курса: https://maxcom.github.io/scala-course-2018/
План поменяем, чтобы успеть к 20 апреля.
Критерии для получения зачета
(нужно выполнить все):
Практическое задание - разработка сервиса эмоциональной оценки текстов соц. сетей
* - "минимальный" функционал
Реализацию начнем на следующем занятии.
Видео с лекций постараемся выложить.
Еще у нас будет стажерская программа.
Переполнение стека в рекурсивном merge
def merge(a1: Seq[Int], a2: Seq[Int]): Seq[Int] = (a1, a2) match {
case (a1_head +: a1_tail, a2_head +: a2_tail) =>
if (a1_head < a2_head) a1_head +: merge(a1_tail, a2)
else a2_head +: merge(a1, a2_tail)
case _ => a1 ++ a2
}
Падает в StackOverflow при более чем ~3500 элементов
Использование длины списка для проверки на пустоту
if (list.length > 0) {
// ...
}
вместо
if (list.nonEmpty) {
// ...
}
Seq бывают разные - в некоторых реализациях length "дорогая" операция
Code Style
Имена переменных пишем в camelCase;
имена типов - MyClassName
Подробнее: Scala Style Guide
В функциональном программировании предпочитают
эти свойства делают код более предсказуемым, а программы - более надежными
Сложности изменяемых данных
object UserRepository {
private val users: ListBuffer[String] = ListBuffer()
def checkPass(user: String, password: String): Boolean = ???
def login(user: String, password: String): Unit = {
if (checkPass(user, password)) {
users += user
} else {
throw new BadPasswordException(user)
}
}
def getUsers(): ListBuffer[String] = users
}
// oops!
UserRepository.getUsers().append("hacker")
аналогичная проблема с данными которые передают в наши функции
def getUsers(): Seq[String] = users.toVector
В Scala это работает лучше, так как попытки модифицировать не компилируются
object UserRepository {
private var users: Vector[String] = Vector.empty
def login(user: String, password: String): Unit = {
if (checkPasword(user, password)) {
users = users :+ user
} else {
throw new BadPasswordException(user)
}
}
def getUsers(): Seq[String] = users
}
Операции создают новую версию; старая остается полностью "рабочей"
Обе версии разделяют общие элементы данных, насколько это возможно
Структура стала популярной благодаря языку Lisp
Используется в Ocaml, Haskell, Erlang и др.
Хороша для обучения, не слишком эффективна на практике
List - алгебраический тип с двумя вариантами:
в Scheme используют просто "пару"
например:
val l: List[Int] = 42 :: 69 :: 613 :: Nil
// для удобства есть конструктор List(42, 69, 613)
(вычисляется справа налево)
* картинка Yonkeltron at English Wikipedia
Два разных списка разделяют общий "хвост"
val list = 5 :: 4 :: 3 :: 2 :: 1 :: Nil
val list2 = 10 :: list
val list3 = 20 :: list
Декомпозиция на голову и хвост – "бесплатная" операция.
Плохой (но популярный) способ:
val l: List[Int] = 42 :: 69 :: 613 :: Nil
if (l.nonEmpty) {
println(l.head) // 42
println(l.tail) // List(69, 613)
}
Используем pattern matching:
l match {
case head :: tail ⇒ // head +: tail
println(head)
println(tail)
case Nil ⇒ // Seq()
println("empty")
}
l match {
// через конструктор - точное число элементов
case Seq(one) ⇒
s"exactly $one"
// как минимум один элемент
case one +: _ ⇒
s"at least one element $one"
// второй элемент 10; выкинем его и преобразуем в строку
case head +: 10 +: tail ⇒
(head +: tail).mkString
}
Декомпозицию можно использовать для итерирования по элементам списка.
Сумма элементов списка
def sum(list: List[Int]): Int = {
list match {
case Nil ⇒
0
case head :: tail ⇒
head + sum(tail)
}
}
сборщик мусора собирает список прямо в процессе итерации (если на него нет ссылок)
Получение "хвоста" начиная с N-ного элемента:
def drop(list: List[Int], n: Int): List[Int] = {
if (n > 0 && list.nonEmpty) {
drop(list.tail, n - 1)
} else {
list
}
}
def dropLoop(list: List[Int], n: Int): List[Int] = {
var these = list
var count = n
while (these.nonEmpty && count > 0) {
these = these.tail
count -= 1
}
these
}
// упрощенная версия кода библиотеки Scala 2.12.4
def apply(n: Int): Int = {
val rest = drop(n)
if (n < 0 || rest.isEmpty)
throw new IndexOutOfBoundsException(n.toString)
rest.head
}
количество операций пропорционально N
def reverse(list: List[Int]): List[Int] = {
def rev(target: List[Int], remains: List[Int]): List[Int] = {
remains match {
case head :: tail ⇒
rev(head :: target, tail)
case Nil ⇒
target
}
}
rev(Nil, list)
}
(легко можно заменить на цикл)
Добавление в хвост - только с созданием полной копии.
def append(list: List[Int], v: Int): List[Int] = {
list match {
case Nil ⇒
v :: Nil
case head :: tail ⇒
head :: append(tail, v)
}
}
число операция пропорционально длине списка
Проблемы этого кода:
Как работает этот код?
Список копируется в стек
Стек раскручивается в новый список
def append(list: List[Int], v: Int): List[Int] = {
(v :: list.reverse).reverse
}
Хвостовая рекурсия — частный случай рекурсии, при котором любой рекурсивный вызов является последней операцией перед возвратом из функции.
Хвостовая рекурсия автоматически заменяется на цикл компилятором (в границах одного файла!)
@tailrec // оптимизация или ошибка
def drop(list: List[Int], n: Int): List[Int] = {
if (n > 0 && list.nonEmpty) {
drop(list.tail, n - 1)
} else {
list
}
}
Одна из "классических" функций: оставляет только элементы, для которых верно условие фильтрации.
// метод List[A]
def filter(pred: A => Boolean): List[A]
Пример использования - только четные числа:
val l: List[Int] = List.fill(10)(Random.nextInt(10))
l.filter(_ % 2 == 0)
эквивалентно
l.filter(x ⇒ x % 2 == 0)
def filter(list: List[Int], f: Int ⇒ Boolean): List[Int] = {
list match {
case head +: tail if f(head) ⇒ head +: filter(tail, f)
case _ +: tail ⇒ filter(tail, f)
case Nil ⇒ Nil
}
}
filter(list, _ % 2 == 0)
рекурсия не хвостовая, стек переполняется
Плохая идея
def filter(list: List[Int], f: Int ⇒ Boolean): List[Int] = {
def rec(list: List[Int], acc: List[Int]): List[Int] = {
list match {
case head +: tail if f(head) ⇒ rec(tail, acc :+ head)
case _ +: tail ⇒ rec(tail, acc)
case Nil ⇒ acc
}
}
rec(list, Nil)
}
Рекурсия хвостовая, но количество операций - пропорционально N*N
Время выполнения:
Классическое решение - сборка списка в обратном порядке и reverse.
Но в стандартной библиотеке Scala (и многих других) код оптимизирован.
List в библиотеке Scala - не совсем immutable:
final case class ::[B](override val head: B,
private[scala] var tl: List[B])
extends List[B]
Однако, для пользовательского кода все гарантии сохранены.
List позволено менять классу ListBuffer - "конструктору" списка.
В ListBuffer хранится приватный список, в который можно добавлять в конец.
Списки, полученные из ListBuffer никогда не меняются.
def filter(list: List[Int])(f: Int ⇒ Boolean): List[Int] = {
val builder = ListBuffer[Int]()
var current = list
while (current.nonEmpty) {
if (f(current.head)) {
builder += current.head
}
current = current.tail
}
builder.toList // после список уже никогда не изменится
}
map - применяет функцию ко всем элементам списка
list.map(x ⇒ x * x)
можно указать готовую функцию:
list.map(Math.sqrt) // для List[Double]
Повторяем каждый элемент списка два раза:
val list = List(1, 2, 3)
list.map(x ⇒ List.fill(2)(x))
// List[List[Int]] = List(List(1, 1), List(2, 2), List(3, 3))
flatten - превращаем список списков в плоский:
val list = List(1, 2, 3)
list.map(x ⇒ List.fill(2)(x)).flatten
// List(1, 1, 2, 2, 3, 3)
flatMap - комбинация map и flatten
list.flatMap(x ⇒ List.fill(2)(x))
Как превратить List[Option[Int]] в List[Int]?
Плохой способ:
val list = List(Some(1), None, Some(3))
list.filter(_.isDefined).map(_.get)
Option.get - источник ошибок
collect - совмещаем map, filter и pattern matching
val list = List(Some(1), None, Some(3))
list collect {
// определяем только интересные паттерны
case Some(v) ⇒ v
}
foldLeft - применяет функцию к начальному значению и элементам списка последовательно
def foldLeft[B](z: B)(op: (B, A) => B): B
z - начальное значение
Сумма элементов списка:
list.foldLeft(0)(_ + _)
отдельно функция:
def op(acc: Int, v: Int) = acc + v
list.foldLeft(0)(op)
op(0, list(0))
op(op(0, list(0)), list(1))
op(op(op(0, list(0)), list(1)), list(2))
op(op(op(op(0, list(0)), list(1)), list(2)), list(3))
...
list.foldLeft(List.empty[Int]) { (acc, v) ⇒
if (acc.headOption.contains(v)) {
acc
} else {
v :: acc
}
}.reverse
Пример: функция count
val list = List.fill(1000)(Random.nextInt(100))
list.count(_ > 50)
Реализация через filter:
def count(list: List[Int], f: Int ⇒ Boolean): Int =
list.filter(f).length
Не эффективно, создает временный список
Через foldLeft:
def count(list: List[Int], f: Int ⇒ Boolean): Int = {
list.foldLeft(0) { (acc, v) ⇒
if (f(v)) {
acc + 1
} else {
acc
}
}
}
еще можно реализовать рекурсивным обходом списка
На 3-м занятии поговорим о других структурах данных
Посмотрите на исследование производительность коллекций Scala: Benchmarking Scala Collections.
Домашнее задание (1/3):
Элементы списка произвольной длины сдигаем влево на N позиций, например:
val list = List('a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k'))
rotate(3, list)
// List('d', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'a', 'b', 'c')
rotate(-2, list)
// List('j', 'k', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')
Разрешено использовать только декомпозицию списка; сборку через ::; fold и reverse
Домашнее задание (2/3):
Реализуем две стандартные функции:
// проверяем существование элемента, удовлетворяющего условию
def exists(list: List[Int], f: Int ⇒ Boolean): Boolean = ???
// проверяет что все элементы удовлетворяют условию
def forall(list: List[Int], f: Int ⇒ Boolean): Boolean = ???
"Short-circuit evaluation" - вычисляем пока не получен результат.
Одну функцию реализуем хвостовой рекурсией, вторую - циклом.
Домашнее задание (3/3):
Проверка корректности решения Судоку - головоломки с числами.
поле представлено списком списков
val sudoku: List[List[Int]] = List(
List(1, 2, 3, 4, 5, 6, 7, 8, 9),
List(4, 5, 6, 7, 8, 9, 1, 2, 3),
List(7, 8, 9, 1, 2, 3, 4, 5, 6),
List(2, 3, 4, 5, 6, 7, 8, 9, 1),
List(5, 6, 7, 8, 9, 1, 2, 3, 4),
List(8, 9, 1, 2, 3, 4, 5, 6, 7),
List(3, 4, 5, 6, 7, 8, 9, 1, 2),
List(6, 7, 8, 9, 1, 2, 3, 4, 5),
List(9, 1, 2, 3, 4, 5, 6, 7, 8)
)
Можно пользоваться любыми функциями библиотеки.
Не забываем про тесты!
дополнительная часть, если успеем
Специальный тип, реализованный в библиотеках Cats, Scalaz и др.
// код библиотеки Cats
final case class NonEmptyList[+A](head: A, tail: List[A])
extends Product with Serializable
Специальная головная ячейка списка
Вопрос: чем это отличается от обычного списка?
final case class NonEmptyList[+A](head: A, tail: List[A])
extends Product with Serializable
Функции которые увеличивают или не меняют длину возвращают NonEmptyList
Остальные возвращают List
import cats.data.NonEmptyList
val list: NonEmptyList[Int] = NonEmptyList.of(1, 2, 3, 4)
val list2: NonEmptyList[Int] = list.map(_ * 2)
val list3: List[Int] = list.tail
зачем?
Например делаем обработчик данных веб-форм на сайте
Простой вариант - функция
def process(request: Request): Result
При ошибках - бросаем исключение
Некорректный ввод - не исключение
def process(request: Request): Either[ValidationError, Result]
Много полей - много разных ошибок
def process(request: Request):
Either[List[ValidationError], Result]
Что делать если обработчик вернул Left[Nil]?
def process(request: Request):
Either[NonEmptyList[ValidationError], Result]
Избегаем ошибки при помощи более точного типа
дополнительная часть, если успеем
У очереди две стандартные операции:
Очередь не изменяемая, помещаем в неё элемент:
import scala.collection.immutable.Queue
var queue = Queue[String]()
queue = queue.enqueue("first")
Обрабатываем элементы
val (next, nextQueueValue) = queue.dequeue
// process(next) - вариант 1
queue = nextQueueValue
// process(next) - вариант 2
Вопрос: в чем разница?
Как это работает? List не подходит
Два приватных списка:
// переформатированный код библиотеки Scala 2.12.4
def dequeue: (A, Queue[A]) = out match {
case Nil if !in.isEmpty =>
val rev = in.reverse
(rev.head, new Queue(Nil, rev.tail))
case x :: xs =>
(x, new Queue(in, xs))
case _ =>
throw new NoSuchElementException("dequeue on empty queue")
}
Проблемы immutable Queue:
дополнительная часть, если успеем
Option напоминает коллекцию, которая или пустая, или содержит один элемент
val v: Option[Int] = Some(11)
v.filter(_ > 5) // Some(11)
v.map(_ * 2) // Some(22)
val users = List(1 -> "Petya", 11 -> "Vasya", 23 -> "Ivan")
v.flatMap(id ⇒ users.find(_._1 == id)).map(_._2)
// Some("Vasya")
С следующий раз поговорим о хеш-таблицах
val usersMap = Map(1 -> "Petya", 11 -> "Vasya", 23 -> "Ivan")
v.flatMap(usersMap.get)