Страничка курса: https://maxcom.github.io/scala-course-2018/
Зачем мы говорили о play-json?
Работа с play-json - знание не слишком ценное:
В прошлый раз мы изучали:
Play-json для нас - только пример.
Почему нет подробной инструкции?
Работать с 100% известной средой – роскошь,
которая бывает редко.
Умение читать документацию, искать в google, stack overflow, т.п. – полезный навык для разработчика.
По этому вместо инструкций – изучаем теорию и причины выбора того или иного решения.
Как разобрать CSV?
Берем вот эту библиотеку:
scala-csv by Toshiyuki Takahashi
"Домашнее" задание: развиваем классификатор
Игнорируем окончания у слов.
Используем готовую библиотеку - Apache Lucene
libraryDependencies +=
"org.apache.lucene" % "lucene-analyzers-common" % "7.2.1"
build.sbt
Создаем анализатор:
val analyzer = new RussianAnalyzer()
В комплекте - токенизатор и стеммер Портера
На нужно достать
case class Term(word: String, start: Int, end: Int)
val ts = analyzer.tokenStream("text", "тестовая строка")
ts.reset()
val out = new ArrayBuffer[Term]
while (ts.incrementToken()) {
val word =
ts.getAttribute(classOf[CharTermAttribute]).toString
val offsets = ts.getAttribute(classOf[OffsetAttribute])
out += Term(word, offsets.startOffset(), offsets.endOffset())
}
out // Term(тестов,0,8), Term(строк,9,15)
Добавляем диагностику
При классификации для каждого класса выбираем 3 характерных слова
Для итогового класса выделяем слова в тексте символами '*'
Пример:
вот вам английский язык! Выучить от сих до сих! Приеду — проверю!
Если *не* выучите — моргалы *выколю*, пасти *порву* и,
как их, эти… носы пооткушу. Ясно?!
В серверных проложениях часто выделяют по потоку каждому клиенту.
Программисту это удобно, но не всегда эффективно. В 6-й лекции поговорим об устройстве высоконагруженных приложений.
val thread = new Thread(() => {
println("Hello world!")
})
thread.start()
Явно потоки (почти) никогда не нужно создавать:
// используем Java API
val executor: ExecutorService =
Executors.newFixedThreadPool(10) // 10 потоков
// создаем Scala-обертку
implicit val ec: ExecutionContextExecutor =
ExecutionContext.fromExecutor(executor)
Задачи можно передать так:
ec.execute(() => {
println("Hello world!")
})
В пуле задачи помещаются в очередь, задачи из которой выполняются в потоках пула.
Модель памяти Java сложна - подробности смотрите Java Memory Model прагматика модели
val lock = new Object()
var counter: Int = 0
lock.synchronized {
counter += 1
}
val lock = new ReentrantReadWriteLock()
var data = Vector(42)
def readData(): Vector[Int] = {
try {
lock.readLock().tryLock(1, TimeUnit.MINUTES)
data
} finally {
lock.readLock().unlock()
}
}
Если брать lock'и в разном порядке в разных потоках можно получить взаимную блокировку, из которой не выйти.
Dealock'ы бывают хитрые, но сводятся к той же схеме.
Пример: извлечение объектов парами из ограниченного пула.
@volatile var vcounter: Int = 0
операции над ней "упорядочены", но защиты от "гонки потоков" нет
Специальная инструкция процессора - CompareAndSet (CAS)
Меняет значение на новое если старое равно заданному
Потокобезопасно
// реализация из исходников JDK; Java
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
Такой подход может использоваться для списков, деревьев и хеш-таблиц.
Разделяемое изменяемое состояние + параллелизм = проблемы.
Нет хорошего способа расставить блокировки в сложной системе.
Разделяемое состояние требует "протокола" доступа, который нужно разработать и потом ему следовать.
Декомпозиция не работает - проблемы возникают в момент интеграции.
Качественно расставить блокировки не всегда удается:
Решение: разделяемое изменяемое состояние + параллелизм.
Делаем всю работу в одном потоке.
Пример: СУБД Redis, веб-сервер nginx, сервер приложений node.js
Решение: разделяемое изменяемое состояние + параллелизм.
Используем иммутабельные структуры и цепочки обработки.
Сегодня об этом поговорим, продолжим на 8-м занятии.
Решение: разделяемое изменяемое состояние + параллелизм.
Изменяемое состояние приватное, работаем с ним из одного потока.
Это подход "Акторов", о которых поговорим на 7-м занятии.
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
val f: Future[Int] = ???
val result: Int = Await.result(f, 5 minutes)
блокирует текущий поток до получения результата
import scala.concurrent.Future
import scala.util.{Failure, Success}
val f: Future[Int] = ???
f.value match {
case Some(Success(value)) ⇒
println(value)
case Some(Failure(ex)) ⇒
throw ex
case None ⇒
println("Not completed :-(")
}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
val f: Future[Int] = ???
implicit val ec: ExecutionContext = ???
f.onComplete { // выполняется в потоке ec
case Success(value) ⇒
println(value)
case Failure(ex) ⇒
println(s"Failed: ${ex.toString}")
}
val p: Promise[Int]
val f: Future[Int] = p.future
f.onComplete {
case Success(value) ⇒
println(value)
case Failure(ex) ⇒
println(s"Failed: ${ex.toString}")
}
p.success(10)
def run[T](f : ⇒ T)
(implicit ec: ExecutionContext): Future[T] = {
val p = Promise[T]()
ec.execute(() ⇒ {
p.complete(Try(f))
})
p.future
}
val f: Future[Int] = run { 2 * 2 }
val f2: Future[Int] = Future {
2 * 2
}
Future - функтор
Функция map
источник: Functors and Applicatives
Законы функтора
композиция:
fa.map(f).map(g) = fa.map(x => g(f(x)))
identity
fa.map(x => x) = fa
Законы для программиста - это "контракт", который обязан реализовать автор функтора.
val f3: Future[Int] = f.map(_ * 10)
функция будет выполнена тогда, когда
implicit class MyFuture[T](val f: Future[T]) extends AnyVal {
def myMap[R](func: T ⇒ R)
(implicit ec: ExecutionContext): Future[R] = {
val p = Promise[R]()
f.onComplete {
case Success(v) ⇒ p.complete(Try(func(v)))
case Failure(ex) ⇒ p.failure(ex)
}
p.future
}
}
Future - монада
flatMap[S](f: T => Future[S]): Future[S]
def userByEmail(email: String): Future[Int] = ???
def ticketsByUser(user: Int): Future[Seq[Int]] = ???
def userActive(user: Int): Boolean =
Set(1, 42).contains(user)
val count: Future[Int] =
userByEmail("user1@test")
.filter(userActive)
.flatMap(ticketsByUser)
.map(_.length)
Можно использовать for:
val count: Future[Int] = for {
user <- userByEmail("user1@test") if userActive(user)
tickets <- ticketsByUser(user)
} yield {
tickets.length
}
Еще полезные методы
Сборка независимых Future
val res: Future[Result] = for {
info <- getUserInfo(user)
stats <- getUserStat(user)
} yield Result(info, stats)
Проблема - задержка вызова getUserStat
val infoF = getUserInfo(user)
val statsF = getUserStat(user)
val res: Future[Result] = for {
info <- infoF
stats <- statsF
} yield Result(info, stats)
Монада - абстракция цепочки вычислений; тут не подходит
Вспомним аппликативный функтор
Классическое определение - функция ap:
источник: Functors and Applicatives
Эквивалентное определение:
product - комбинирует два функтора
в функтор от пары
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
Future - аппликативный функтор
(в библиотеке Cats)
import cats.instances.future._
import cats.syntax.all._
(getUserInfo(user), getUserInfo(user)).mapN(Result.apply)
Future.sequence для списков
def process(i: Int): Future[Int] = ???
val processed: Seq[Future[Int]] =
Seq(1, 2, 3).map(process)
val completed: Future[Seq[Int]] =
Future.sequence(processed)
Future.sequence может быть опасен - загружает очередь пула потоков и может "перегрузить" process()
Дополнительно:
трансформер OptionT
Мы можем использовать for для Future[T] и для Option[T]
Что делать с Future[Option[T]]?
Что мы хотим?
Универсального способа объединить две монады нет.
Но можно сделать такой flatMap:
class FutureO[+A](val future: Future[Option[A]]) extends AnyVal {
def flatMap[B](f: A ⇒ FutureO[B])
(implicit ec: ExecutionContext): FutureO[B] = {
val newFuture = future.flatMap {
case Some(a) ⇒ f(a).future
case None ⇒ Future.successful(None)
}
new FutureO(newFuture)
}
}
Что в этом коде от Option?
Логика работы.
Что в этом коде от Future?
flatMap и функция создания
На месте Future может быть любая другая монада.
В библиотеке cats есть трансформер OptionT
val greetingFO: Future[Option[String]] = ???
val firstnameF: Future[String] = ???
val lastnameO: Option[String] = ???
val ot: OptionT[Future, String] = for {
g <- OptionT(greetingFO)
f <- OptionT.liftF(firstnameF)
l <- OptionT.fromOption[Future](lastnameO)
} yield s"$g $f $l"
val result: Future[Option[String]] = ot.value
Трансформеры есть и для некоторых других монад.
Напоминаю: