За последние несколько лет я видел много дискуссий о функциях Kotlin. Среди обсуждаемых тем обнуляемость всегда в первых рядах. Мне она нравится, но это определенно не моя любимая функция.
Больше всего в Kotlin мне нравятся некоторые мелкие особенности, благодаря которым язык становится очень элегантным. Например, смарт-кастинг (контракты), приостановка, функции приемника/расширения и делегирование.
Но на данный момент моей любимой особенностью является сочетание функций inline и suspend.
Функцией inline (осознанно или нет) регулярно пользуется каждый, работающий с Kotlin. Функция может быть помечена как inline, если в качестве параметров у нее есть лямбда-выражения, также известные как функции высшего порядка. Функция высшего порядка — это функция, которая принимает другие функции в качестве параметров или возвращает новую функцию. Мы называем map функцией высшего порядка, поскольку она принимает transform другой функции в качестве параметра.
Iterable.map, вероятно, самый известный из таких вариантов. Он отображает все элементы списка типа A на список типа B с помощью функции transform (A) -> B. Когда эти паттерны были впервые введены в обращение, возникло некоторое беспокойство по поводу производительности, поскольку for-циклы более эффективны на уровне байт-кода.
inline — это возможность компилятора Kotlin, которая позволяет полностью избавиться от вышеупомянутой проблемы. Если мы посмотрим на тело функции Iterable.map, то увидим for-цикл.
Итак, давайте посмотрим, как выглядит байт-код listOf(1, 2).map(Int::toString):
Точно как если бы мы написали цикл for вручную. Потрясающе, мы можем писать высокоуровневый код, не теряя ни одного из преимуществ написания низкоуровневого кода!
Однако инлайнинг – это не уникальная черта Kotlin. Многие другие языки также поддерживают инлайнинг.
Функция suspend (приостановка) в Kotlin является чрезвычайно мощным инструментом, и есть много интересных вариантов использования. Приостановка позволяет обертывать функции обратного вызова и писать императивный код поверх обратных вызовов.
В конкурентном режиме это означает, что мы можем работать с JVM Future или JS Promise, и нам при этом не требуется писать код, основанный на обратных вызовах. Это очень эффективно, поскольку теперь мы можем создавать «обычный императивный код» для описания мощных асинхронных рабочих процессов, но это также позволяет использовать очень интересные сценарии, не сводящиеся к конкурентности. Отличный пример подробно рассмотрен в моем предыдущем посте в блоге.
Компилятор также убеждается, что приостанавливающий код может быть вызван только из другого приостанавливающего кода, так что вы просто не сможете случайно вызвать приостанавливающий код из мест, которые его не поддерживают.
Функция suspend также известна как «стиль с передачей продолжений», что означает, что компилятор может автоматически передавать продолжения от функции к функции за нас. Он делает это чрезвычайно эффективно, поэтому можно быстро писать код, основанный на обратных вызовах.
Теперь, когда мы обсудили, как работают операции suspend и inline, мы можем рассмотреть мою любимую возможность Kotlin — поддержку suspend + inline!
Давайте начнем с примера без инлайнинга.
Этот код не компилируется, и выдается исключение «Suspension functions can be called only within coroutine body» («функция приостановки может вызываться только внутри тела корутины»), даже если наша функция из примера помечена как suspend. Это происходит потому, что мы создаем непрерывное лямбда-преобразование для передачи в функцию map, и изнутри этой непрерывной лямбды мы не можем вызвать приостанавливающую функцию fetchUser.
Но когда мы помечаем эту функцию как inline, она, оказывается, работает. Все потому, что она больше не создает непрерывную лямбду, а вместо этого в нашем примере мы снова имеем оптимизированный for-цикл, из которого вызывается функция resolve.
Поскольку компилятор знает, что непрерывная лямбда не будет существовать из-за inline, он позволяет вызывать suspend изнутри. Это похоже на smart-casting, компилятор имеет определенные знания о программе, что позволяет нам делать то, что обычно не разрешается.
При проектировании библиотек или функций высокого порядка важно помнить о inline. Не только из соображений производительности, но и из соображений приостановки.
Это одна из особенностей компилятора Kotlin, которую легко контролировать, поскольку она работает настолько очевидно.
Рассмотрим другие интересные примеры и ряд нюансов.
В приведенном выше примере у нас есть список из 10 элементов, и мы хотим обработать каждый элемент. При использовании Iterable.map мы получаем List<Either<String, Int>>, что не так интересно. На самом деле нам нужно получить Either<String, List>, либо первую ошибку, либо все обработанные результаты.
Если вы знакомы с Arrow, то видели такой DSL, в котором можно использовать suspend для работы над Either.Right более удобным способом. Поскольку можно сочетать inline с suspend, мы можем извлечь значение из Either внутри Iterable.map.
Теперь мы получаем интересующий нас результат в Either<String, List>, либо первую ошибку, либо все обработанные результаты. Если вы знакомы с функциональным программированием, вы можете заметить, что это заменяет иерархию Traverse и Monad. Таким образом, практикуя функциональное программирование в Kotlin, мы можем забыть о многих сложностях, поскольку можно добавить хороший семантический сахар, используя приостановку. Примечание: здесь мы используем either.eager, которая использует suspend, но не допускает никакого постороннего приостанавливающего кода.
Другой интересный пример — Sequence из стандартной библиотеки Kotlin, где на основе suspend создаётся мощный DSL для построения ленивых последовательностей. sequence предлагает вариант suspend fun yield(a: A):Unit, который может использоваться для выпуска/выдачи ( emit/yield) значения в Sequence. Ниже мы можем использовать его для реализации flatMap через suspend и inline.
1. Откройте блок sequence в DSL
2. Переберите все элементы в Sequence<А>
3. Преобразуйте A в Sequence<В>
4. Переберите все элементы Sequence<В> и извлеките их.
Причина, по которой мы можем вызвать yield из двойного вложенного forEach, заключается в том, что они оба помечены как inline, и поэтому мы можем безопасно вызывать yield изнутри.
К сожалению, этот паттерн не слишком надёжен. Есть некоторые проблемы, касающиеся безопасности ресурсов и отмены.
Этот код не является безопасным при отмене, и, если выполнение задачи срывается, она никогда не сможет отменить другие задачи, стоящие в списке раньше неё. Это связано с ожиданием отложенных задач в порядке их следования в списке, а не всех одновременно. Для одновременного ожидания всех отложенных задач следует использовать awaitAll…
Такое органичное сочетание этих двух функций, на мой взгляд, является недооцененной особенностью компилятора Kotlin и одной из моих любимых.
Планируемая функция в Kotlin, Context receivers, сделает этот паттерн еще более эффективным.
Спасибо, что прочитали этот пост о моей любимой функции Kotlin. Надеюсь, вам понравилось, и счастливого Нового года!
Больше всего в Kotlin мне нравятся некоторые мелкие особенности, благодаря которым язык становится очень элегантным. Например, смарт-кастинг (контракты), приостановка, функции приемника/расширения и делегирование.
Но на данный момент моей любимой особенностью является сочетание функций inline и suspend.
Обзор функции inline
Функцией inline (осознанно или нет) регулярно пользуется каждый, работающий с Kotlin. Функция может быть помечена как inline, если в качестве параметров у нее есть лямбда-выражения, также известные как функции высшего порядка. Функция высшего порядка — это функция, которая принимает другие функции в качестве параметров или возвращает новую функцию. Мы называем map функцией высшего порядка, поскольку она принимает transform другой функции в качестве параметра.
inline fun <A, B> Iterable<A>.map(transform: (A) -> B): List<B>
Iterable.map, вероятно, самый известный из таких вариантов. Он отображает все элементы списка типа A на список типа B с помощью функции transform (A) -> B. Когда эти паттерны были впервые введены в обращение, возникло некоторое беспокойство по поводу производительности, поскольку for-циклы более эффективны на уровне байт-кода.
inline — это возможность компилятора Kotlin, которая позволяет полностью избавиться от вышеупомянутой проблемы. Если мы посмотрим на тело функции Iterable.map, то увидим for-цикл.
Итак, давайте посмотрим, как выглядит байт-код listOf(1, 2).map(Int::toString):
val destination = ArrayList<String>()
for (item in listOf(1, 2)) {
destination.add(item.toString())
}
Точно как если бы мы написали цикл for вручную. Потрясающе, мы можем писать высокоуровневый код, не теряя ни одного из преимуществ написания низкоуровневого кода!
Однако инлайнинг – это не уникальная черта Kotlin. Многие другие языки также поддерживают инлайнинг.
Обзор функции suspend
Функция suspend (приостановка) в Kotlin является чрезвычайно мощным инструментом, и есть много интересных вариантов использования. Приостановка позволяет обертывать функции обратного вызова и писать императивный код поверх обратных вызовов.
suspend fun example(): Int = suspendCoroutine { continuation ->
callbackCode { int, error ->
if(error == null) continuation.resume(int)
else continuation.resumeWithException(error)
}
}
suspend fun twice() = example() + example()
В конкурентном режиме это означает, что мы можем работать с JVM Future или JS Promise, и нам при этом не требуется писать код, основанный на обратных вызовах. Это очень эффективно, поскольку теперь мы можем создавать «обычный императивный код» для описания мощных асинхронных рабочих процессов, но это также позволяет использовать очень интересные сценарии, не сводящиеся к конкурентности. Отличный пример подробно рассмотрен в моем предыдущем посте в блоге.
Компилятор также убеждается, что приостанавливающий код может быть вызван только из другого приостанавливающего кода, так что вы просто не сможете случайно вызвать приостанавливающий код из мест, которые его не поддерживают.
suspend fun example(): Int = suspendCoroutine { continuation ->
callbackCode { int, error ->
if(error == null) continuation.resume(int)
else continuation.resumeWithException(error)
}
}
fun once() = example()
// Невозможно вызвать пример приостанавливающего fun из не приостанавливающего fun один раз
Функция suspend также известна как «стиль с передачей продолжений», что означает, что компилятор может автоматически передавать продолжения от функции к функции за нас. Он делает это чрезвычайно эффективно, поэтому можно быстро писать код, основанный на обратных вызовах.
suspend + inline
Теперь, когда мы обсудили, как работают операции suspend и inline, мы можем рассмотреть мою любимую возможность Kotlin — поддержку suspend + inline!
Давайте начнем с примера без инлайнинга.
fun <A, B> Iterable<A>.map(transform: (A) -> B): List<A> = TODO()
suspend fun fetchUser(id: Int): Result<Int> = Result.success(id)
suspend fun example5() = listOf(1, 2, 3).map { fetchUser(it) }
Этот код не компилируется, и выдается исключение «Suspension functions can be called only within coroutine body» («функция приостановки может вызываться только внутри тела корутины»), даже если наша функция из примера помечена как suspend. Это происходит потому, что мы создаем непрерывное лямбда-преобразование для передачи в функцию map, и изнутри этой непрерывной лямбды мы не можем вызвать приостанавливающую функцию fetchUser.
Но когда мы помечаем эту функцию как inline, она, оказывается, работает. Все потому, что она больше не создает непрерывную лямбду, а вместо этого в нашем примере мы снова имеем оптимизированный for-цикл, из которого вызывается функция resolve.
Поскольку компилятор знает, что непрерывная лямбда не будет существовать из-за inline, он позволяет вызывать suspend изнутри. Это похоже на smart-casting, компилятор имеет определенные знания о программе, что позволяет нам делать то, что обычно не разрешается.
При проектировании библиотек или функций высокого порядка важно помнить о inline. Не только из соображений производительности, но и из соображений приостановки.
Это одна из особенностей компилятора Kotlin, которую легко контролировать, поскольку она работает настолько очевидно.
Ещё несколько примеров
Рассмотрим другие интересные примеры и ряд нюансов.
fun process(i: Int): Either<String, Int> = i.right()
val res: List<Either<String, Int>> = (0..9).map { process(it) }
В приведенном выше примере у нас есть список из 10 элементов, и мы хотим обработать каждый элемент. При использовании Iterable.map мы получаем List<Either<String, Int>>, что не так интересно. На самом деле нам нужно получить Either<String, List>, либо первую ошибку, либо все обработанные результаты.
Если вы знакомы с Arrow, то видели такой DSL, в котором можно использовать suspend для работы над Either.Right более удобным способом. Поскольку можно сочетать inline с suspend, мы можем извлечь значение из Either внутри Iterable.map.
fun process(i: Int): Either<String, Int> = i.right()
val res: Either<String, List<Int>> = either.eager<String, Int> {
(0..9).map { process(it).bind() }
}
Теперь мы получаем интересующий нас результат в Either<String, List>, либо первую ошибку, либо все обработанные результаты. Если вы знакомы с функциональным программированием, вы можете заметить, что это заменяет иерархию Traverse и Monad. Таким образом, практикуя функциональное программирование в Kotlin, мы можем забыть о многих сложностях, поскольку можно добавить хороший семантический сахар, используя приостановку. Примечание: здесь мы используем either.eager, которая использует suspend, но не допускает никакого постороннего приостанавливающего кода.
Другой интересный пример — Sequence из стандартной библиотеки Kotlin, где на основе suspend создаётся мощный DSL для построения ленивых последовательностей. sequence предлагает вариант suspend fun yield(a: A):Unit, который может использоваться для выпуска/выдачи ( emit/yield) значения в Sequence. Ниже мы можем использовать его для реализации flatMap через suspend и inline.
fun <A, B> Sequence<A>.flatMap(transform: (A) -> Sequence<B>): Sequence<B> =
sequence {
forEach { a: A ->
f(a).forEach { b: B -> yield(b) }
}
}
1. Откройте блок sequence в DSL
2. Переберите все элементы в Sequence<А>
3. Преобразуйте A в Sequence<В>
4. Переберите все элементы Sequence<В> и извлеките их.
Причина, по которой мы можем вызвать yield из двойного вложенного forEach, заключается в том, что они оба помечены как inline, и поэтому мы можем безопасно вызывать yield изнутри.
Нюансы
К сожалению, этот паттерн не слишком надёжен. Есть некоторые проблемы, касающиеся безопасности ресурсов и отмены.
coroutineScope {
listOf(1, 2, 3)
.map { async { ... } }
.map { it.await() }
}
Этот код не является безопасным при отмене, и, если выполнение задачи срывается, она никогда не сможет отменить другие задачи, стоящие в списке раньше неё. Это связано с ожиданием отложенных задач в порядке их следования в списке, а не всех одновременно. Для одновременного ожидания всех отложенных задач следует использовать awaitAll…
coroutineScope {
listOf(1, 2, 3)
.map { async { ... } }
.awaitAll()
}
Заключение
Такое органичное сочетание этих двух функций, на мой взгляд, является недооцененной особенностью компилятора Kotlin и одной из моих любимых.
Планируемая функция в Kotlin, Context receivers, сделает этот паттерн еще более эффективным.
Спасибо, что прочитали этот пост о моей любимой функции Kotlin. Надеюсь, вам понравилось, и счастливого Нового года!