Приветствую! В этой статье будет показано, как, имея на руках обычные Future-ы, сделать в scala подобие корутин и асинхронные stream-ы. Этакий небольшой туториал по функциональному программированию.
Что это и зачем
Что такое Future человеческим языкомFuture — это сущность, описывающая результат некоторых вычислений, который мы получим не сразу, но в будущем. Но есть одна особенность: зачастую мы, не зная еще результата, точно знаем, что мы с ним будем делать. Например, мы попросили у сервера какой-то конфиг, и теперь у нас есть Future[Config]. Сам конфиг мы еще не получили, но точно знаем, что, когда получим, то достанем из него адрес и по этому адресу попросим у сервера картинку (config => Future[Image]). И Future[Config] способна изменяться таким образом, чтобы мы вместо конфига и потом картинки могли получить сразу картинку. Сущности, способные комбинироваться таким способом, называются монадами.
К сожалению, простое последовательное комбинирование 2х и более асинхронных операций (загрузить конфиг, а потом картинку по адресу из конфига как пример) — это все, на что способны обычные Future-ы в качестве монад. Они не позволяют ни сохранять состояние, ни делать циклы из асинхронных операций, ни выдавать несколько (или бесконечно много) значений. Вот этими недостатками мы сейчас и займемся.
Давайте для определенности представим некий виджет. Он ждет конфиг, который обновляется с определенной периодичностью, загружает по адресу из конфига значение (например, температуру), и рисует на экране текущее значение, минимум, максимум, среднее и так далее. И все это делает в цикле, да еще и асинхронно.
Применив знания из этой статьи, мы сможем этот процесс описать примерно так:
Код// Про 'FState' - далее, пока же просто примем, что это - такая необычная Future
def getNextConfig: FState[Config]
def getTemperature(from: String): FState[Int]
case class State(temperature: Int, sumTemp: Long, count: Int) {
def isGood = ...
}
// Как видим, получается единый асинхронный алгоритм с состоянием,
// которое извне этого алгоритма не видно
val handle =
while_ ( _.isGood)
{ for (
config <- getNextConfig();
if (config.isDefined); // пустой конфиг - прекращаем выполнение
nextValue <- getTemperature(config().source); // грузим значение температуры
state <- gets[State]; // тут мы берем текущее состояние
newState = State(nextValue, state.sumTemp + nextValue, state.count + 1);
_ <- puts(newState); // .. и меняем его
_ <- runInUiThread { drawOnScreen(newState) }
) yield() }
Или вот так:
Кодval configs: AsyncStream[Config] = ... // получаем откуда-то stream конфигов
def getTemperature(from: String): FState[Int]
case class State(temperature: Int, sumTemp: Long, count: Int)
// Получается то же самое, только вместо зависимости 'getNextConfig'
// мы, по сути, передаем сами данные - stream из конфигов
val handle =
foreach(configs) {
config => for (
nextValue <- getTemperature(config().source); // грузим значение температуры
state <- gets[State]; // тут мы берем текущее состояние
newState = State(nextValue, state.sumTemp + nextValue, state.count + 1);
_ <- puts(newState); // .. и меняем его
_ <- runInUiThread { drawOnScreen(newState) }
) yield()
}
Всех, кто заинтересовался, прошу под кат.