Этот пост вдохновлен постом Давида Карнок RxJava vs. Kotlin Coroutines, быстрый взгляд. Давайте глубже посмотрим, как легко создавать абстракции высшего порядка с помощью функций высшего порядка и сопрограмм Kotlin.

Проблема

Дэвид сформулировал следующую проблему:

Допустим, у нас есть две функции, имитирующие ненадежное обслуживание: f1 и f2, обе возвращающие число после некоторой задержки. Мы должны вызвать эти службы, просуммировать их возвращенные значения и представить их пользователю. Однако, если это не произойдет в течение 500 миллисекунд, мы не ожидаем, что это произойдет достаточно быстро, поэтому мы хотели бы отменить и повторить две службы в течение ограниченного времени, прежде чем отказаться от определенного количества повторных попыток.

Следующие ниже функции имитируют эти услуги и их ненадежный характер. Первым параметром этих функций является номер попытки (начиная с единицы).

suspend fun f1(i: Int): Int {
    println("f1 attempt $i")
    delay(if (i != 3) 2000 else 200)
    return 1
}

suspend fun f2(i: Int): Int {
    println("f2 attempt $i")
    delay(if (i != 3) 2000 else 200)
    return 2
}

На практике мы выполняем некоторые асинхронные сетевые операции в f1 и f2. Мы имитируем асинхронную операцию с задержкой, которая приостанавливает выполнение на указанное количество миллисекунд. Эти функции работают достаточно быстро только с третьей попытки.

И f1, и f2 могут быть отменены в силу возможности отмены delay, точно так же, как и в реальной асинхронной сетевой операции, что важно для решения, которое представлен ниже.

Решение

Перейдем к функциональному программированию и решим эту проблему путем композиции функций высшего порядка. Один из таких функциональных строительных блоков уже предоставлен kotlinx.coroutines библиотекой как функция высшего порядка withTimeout. Мы используем его для реализации нашей собственной функции высшего порядка, которая инкапсулирует желаемую логику повтора:

suspend fun <T> retry(block: suspend (Int) -> T): T {
    for (i in 1..5) { // try 5 times 
        try {
            return withTimeout(500) { // with timeout
                block(i)
            }
        } catch (e: TimeoutCancellationException) { /* retry */ }
    }
    return block(0) // last time just invoke without timeout
}

Приведенный выше код легко понять, потому что он напрямую представляет логику повторных попыток, которую мы хотели бы достичь, но не привязан к конкретной функции, поэтому теперь мы можем выполнять как retry { f1(it) }, так и retry { f2(it) }.

И функция retry, и ее параметр block помечены модификатором suspend. Это единственное различие между этим кодом и кодом, который мы могли бы написать, если бы проблема была указана для синхронных, блокирующих функций f1 и f2 вместо этого. Это главное ценностное предложение Kotlin Coroutines - вам не нужно изучать совершенно новый язык, чтобы составлять свои асинхронные вычисления. Вы можете повторно использовать подходы и решения из мира блокировок.

Мы хотим одновременно выполнять как f1, так и f2. Руководящий принцип kotlinx.coroutines дизайна заключается в том, что параллелизм должен быть явным, поэтому для подобных случаев существует отдельная функция высшего порядка с именем async.

async { ... } запускает асинхронное вычисление и продолжает одновременно выполнять следующий оператор, возвращая отложенное значение, которое мы можем await позже, чтобы получить результат начатого вычисления.

fun main(args: Array<String>) = runBlocking {
    val time = measureTimeMillis {
        val v1 = async { retry { f1(it) } }
        val v2 = async { retry { f2(it) } }
        println("Result = ${v1.await() + v2.await()}")
    }
    println("Completed in $time ms")
}

Выполнение приведенного выше кода дает ожидаемый результат 3 и ожидаемое время выполнения около 1200 мс (два тайм-аута по 500 мс каждый плюс третий, успешное выполнение за 200 мс):

f2 attempt 1
f1 attempt 1
f2 attempt 2
f1 attempt 2
f2 attempt 3
f1 attempt 3
Result = 3
Completed in 1234 ms

В этой сути доступен весь код.

дальнейшее чтение

Если вы новичок в мире Kotlin Coroutines и находите некоторые из представленных здесь концепций новыми или запутанными, вам следует подумать о просмотре выступления Введение в Coroutines на KotlinConf 2017 и чтении Руководства по kotlinx.coroutines на примерах.