Сегодня расскажем про библиотеку Arrow в Kotlin. Arrow привносит в Kotlin крутые штуки из функционального программирования: типы вроде Either и Validated для работы с ошибками, эффекты для безопасной работы с внешним миром, ну и много всего. Звучит мощно, но когда за это действительно стоит браться?
Начнем с краткого экскурса для тех, кто не в курсе. Arrow.kt — это библиотека, расширяющая возможности Kotlin в стиле функционального программирования.
Arrow пытается сделать код безопаснее, выразительнее и надежнее.
Но за все приходится платить. Переход на функциональный стиль требует привыкания команды, а местами код становится хитрее, чем привычный императивный. Поэтому важно понять — когда игра стоит свеч.
Either: вместо исключений и null
В обычном Kotlin‑коде для обработки ошибок мы часто либо бросаем исключения, либо возвращаем null/Result. Either из Arrow уже другой подход: функция возвращает либо успешный результат (Right), либо ошибку (Left). Т
Допустим, есть три шага: получить данные пользователя из БД, затем вычислить для него какую‑нибудь метрику, затем сохранить отчет. В императивном стиле это был бы вложенный try‑catch или проверки на null:
fun generateReport(userId: Int): String {
val user = loadUserFromDb(userId) ?: return "Пользователь не найден"
val metric = calculateMetric(user) ?: return "Не удалось вычислить метрику"
return try {
saveReport(user, metric)
"Отчет сохранен"
} catch (e: Exception) {
"Ошибка сохранения отчета: ${e.message}"
}
}Код работает, но смешивает логику и обработку ошибок, читается тяжеловато. Посмотрим на вариант с Arrow и Either:
fun loadUser(userId: Int): Either<Error, User> { ... }
fun calculateMetric(user: User): Either<Error, Double> { ... }
fun saveReport(user: User, metric: Double): Either<Error, Unit> { ... }
fun generateReportArrow(userId: Int): Either<Error, String> = either {
val user = loadUser(userId).bind()
val metric = calculateMetric(user).bind()
saveReport(user, metric).bind()
// Если до сюда дошли, значит все предыдущие .bind() не "выпали" с ошибкой
"Отчет сохранен успешно"
}Описываем последовательность действий в блоке either { }. Вызывая .bind(), мы как бы говорим: «если в Left ошибка, прекратить выполнение и вернуть ее из всей функции». Если же .bind() вернулось успешно (Right), получаем значение и идем дальше. В итоге generateReportArrow вернет либо Right("Отчет сохранен успешно"), либо какой‑то Left(Error) с причиной, почему не вышло.
Код выше уже не бросает исключений вообще, все ошибки типизированы нашим собственным классом Error (может быть sealed class или enum с вариантами ошибок). И главное, читаться стало линейнее, без лесенки из if/try. Похожий стиль в других языках называют монадическим (в Haskell это do‑нотация, в Scala — for comprehension). В Kotlin, благодаря Arrow, мы получили почти то же самое, просто используя знакомый синтаксис.
Когда стоит использовать Either? На мой взгляд:
Когда нужно надежно контролировать каждую ошибку
В многошаговых операциях
В библиотечном или модульном коде. Если вы пишете SDK или модуль, лучше возвращать
Either(или хотя бы стандартныйResult), чем кидать исключения.
А когда не нужно? Если логика простая, то все равно используются исключения (например, метод из стандартной библиотеки кидает IOException), оборачивать их в Either ради галочки смысла мало.
Кстати, в самом Kotlin есть тип Result, который напоминает Either (только Left там не обобщенный, а всегда Throwable). В некоторых случаях его достаточно.
Validated: собираем все ошибки сразу
Перейдем к Validated. Ситуация: нужно проверить набор полей и сообщить пользователю обо всех выявленных ошибках сразу. Стандартный путь — проверить поле А, при ошибке вернуть ее, иначе проверить B, и так далее. В результате пользователь получает по одной ошибке за раз: исправил имя, получил новую про возраст, потом про email… Долго и глуповато. Гораздо лучше сразу выдать список: «Поле Name пустое, Age вне диапазона, Email в неверном формате».
Validated как раз для этого случая. Это структура, которая, в отличие от Either, не останавливается на первой же ошибке. Она аккумулирует все ошибки, но при этом, если все хорошо, несет валидное значение.
В Arrow есть хороший способ валидировать сразу несколько значений. Представим, есть форма с тремя полями: имя, возраст и email. Напишем функции проверки для каждого поля, возвращающие ValidatedNel<ValidationError, T> (Nel означает Non‑Empty List, то есть список ошибок):
data class UserInput(val name: String, val age: Int, val email: String)
sealed class ValidationError(val msg: String) {
object NameEmpty : ValidationError("Имя не должно быть пустым")
object AgeOutOfRange : ValidationError("Возраст должен быть от 0 до 150")
object EmailInvalid : ValidationError("Email некорректный")
}
fun validateName(name: String): ValidatedNel<ValidationError, String> =
if (name.isNotBlank()) name.validNel() else ValidationError.NameEmpty.invalidNel()
fun validateAge(age: Int): ValidatedNel<ValidationError, Int> =
if (age in 0..150) age.validNel() else ValidationError.AgeOutOfRange.invalidNel()
fun validateEmail(email: String): ValidatedNel<ValidationError, String> =
if (Regex("^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,6}\$").matches(email)) {
email.validNel()
} else {
ValidationError.EmailInvalid.invalidNel()
}Каждая функция возвращает либо Valid (валидное значение), либо Invalid (с одной ошибкой, которая упакована в NonEmptyList). Теперь, чтобы собрать результаты трех проверок, Arrow имеет метод .zip:
fun validateUserInput(input: UserInput): ValidatedNel<ValidationError, UserInput> =
validateName(input.name)
.zip(validateAge(input.age), validateEmail(input.email)) { name, age, email ->
input // можно вернуть сам объект или собрать новый с проверенными данными
}Метод zip вызовет наши три проверки параллельно и соберет все ошибки, если они есть, в один список. Если хотя бы одна проверка вернет Invalid, на выходе получим Invalid(NonEmptyList<ValidationError>) со всеми ошибками. Если же все три вернули Valid, zip передаст их результаты в лямбду и вернет Valid с нашим объектом (тут я для простоты возвращаю тот же input).
Вообще, Validated юзают в проектах, где много бизнес‑правил и нужно показать пользователю все проблемы разом. Например, валидация сложной формы заказа: если у клиента 5 ошибок в данных, зачем гонять его 5 раз по кругу? Можно собрать все и облегчить жизнь и ему, и нам. В то же время, если ошибок максимум одна‑две и они простые,можно обойтись и без Arrow, написав пару if и собирая сообщения вручную. Все по мере сложности задачи.
suspend и Arrow Fx
В идеальном функциональном мире любая функция с сайд‑эффектом должна быть обернута в специальный тип, чтобы мы не потеряли контроль над ее выполнением. В Haskell для этого есть монада IO, в Scala тип IO из Cats Effect. А что же в Kotlin?
До недавнего времени Arrow имел свой тип IO для описания эффектов. Но в актуальных версиях ставка сделана на нативные suspend функции Kotlin и корутины. Arrow предлагает считать любую suspend fun аналогом эффекта. А для дополнительной функциональности есть модуль arrow‑fx‑coroutines.
Например:
suspend fun fetchUser(id: Int): Either<Throwable, User> = either {
val response = api.getUserById(id).bind()
// api.getUserById возвращает Either<Throwable, User>
// Если ошибка — .bind прервёт выполнение блока
User(response.id, response.name)
}Вызываем некую api.getUserById, которая возвращает Either. Благодаря either { } блоку нам не нужно вручную проверять результат — .bind() сделает это за нас, если придет Left. В итоге fetchUser сама возвращает Either. Мы получаем suspend‑функцию, которая не кидает исключений наружу, а всегда возвращает либо готового пользователя, либо ошибку (например, исключение, завернутое в Left).
Arrow Fx имеет и более крутые возможности: например, функцию parZip для параллельного выполнения нескольких suspend‑операций с объединением результатов. Или тип Resource для безопасного управления ресурсами.
Однако сам Kotlin и так имеет отличные корутины, необходимость в Arrow Fx не столь остра, как, скажем, в Java или Scala. Многие вещи решаются штатными средствами языка или библиотекой kotlinx.coroutines.

Если Kotlin для вас — уже не про синтаксис, а про зрелые решения и ответственность кода, дальше неизбежно встает вопрос архитектуры и инструментов. Курс Android Developer как раз про этот переход: как с нуля научиться создавать Android-приложения, работать с корутинами, зависимостями, тестами и сборкой, не теряя контроль по мере роста сложности.
Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные демо-уроки:
5 февраля в 20:00. «Основные компоненты приложения Android». Записаться
17 февраля в 20:00. «От API до экрана: создаём Android-приложение на рекомендуемой архитектуре». Записаться
