В функциональном программировании предпочитают
эти свойства делают код более предсказуемым, а программы - более надежными
Сложности изменяемых данных
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
}
Операции создают новую версию; старая остается полностью "рабочей"
Обе версии разделяют общие элементы данных, насколько это возможно
List[T] - алгебраический тип с двумя вариантами:
в 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
}
Хвостовая рекурсия — частный случай рекурсии, при котором любой рекурсивный вызов является последней операцией перед возвратом из функции.
Хвостовая рекурсия автоматически заменяется на цикл компилятором (в границах одного файла!)
@tailrec // оптимизация или ошибка
def drop(list: List[Int], n: Int): List[Int] = {
if (n > 0 && list.nonEmpty) {
drop(list.tail, n - 1)
} else {
list
}
}
// упрощенная версия кода библиотеки 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 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
}
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)
}
(легко можно заменить на цикл)
Одна из "классических" функций: оставляет только элементы, для которых верно условие фильтрации.
// метод 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 // после список уже никогда не изменится
}
Builder - абстракция для "конструкторов" неизменяемых типов
val builder = Vector.newBuilder[Int]
builder += 1
builder.result()
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.flatMap(List.fill(2))
Как превратить List[Option[Int]] в List[Int]?
(без flatten)
Плохой способ:
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
}
for тоже так может:
val list = List(Some(1), None, Some(3))
for {
Some(v) <- list
} yield 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
}
}
}
еще можно реализовать рекурсивным обходом списка
trait SimpleTreeSet {
def +(v: Int): SimpleTreeSet
def contains(v: Int): Boolean
def foreach(f: Int => Unit): Unit
}
Стартовый код в репозитории
(каталог code/seminar2).
Заранее откройте его в IDEA. Запустите командой
sbt "jmh:run -f 0 -wi 1 -i 1"
Напоминаю:
дополнительная часть, если успеем
Специальный тип, реализованный в библиотеках Cats, Scalaz и др.
// код библиотеки Cats
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")
Напоминаю: