Хабрахабр

Функциональная обработка ошибок в Kotlin с помощью Arrow

image

Привет, Хабр!

Нет лучшего способа узнать о том, что что-то не было учтено при написании кода. Все любят runtime exceptions. В субботу утром. Особенно — если исключения обваливают приложение у миллионов пользователей, и эта новость приходит паническим email'ом с портала аналитики. Когда ты в загородной поездке.

После подобного всерьез задумываешься о обработке ошибок — и какие же возможности предоставляет нам Kotlin?

По мне — отличный вариант, но у него есть две проблемы: Первым на ум приходит try-catch.

  1. Это как-никак лишний код (вынужденная обертка вокруг кода, не лучшим образом сказывается на читаемости).
  2. Не всегда (особенно при использовании сторонних библиотек) из блока catch возможно получить информативное сообщение о том, что конкретно вызвало ошибку.

Давайте посмотрим во что try-catch превращает код при попытке решения вышеозвученных проблем.
Например, простейшая функция выполнения сетевого запроса

fun makeRequest(request: RequestBody): List<ResponseData>? else { null }
}

становится похожа на

fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null }
}

«Не так уж и плохо», может сказать кто-то, «вам с вашим котлином всё кодового сахарку хочется», добавит он (это цитата) — и будет… дважды прав. Нет, холиваров сегодня не будет — каждый решает за себя. Я лично правил код самописного json парсера, где парсинг каждого поля был завернут в try-catch, при этом каждый из блоков catch был пустым. Если кого-то устраивает подобное положение вещей — флаг в руки. Я же хочу предложить способ лучше.

Try для обработки исключений, a Either для обработки ошибок бизнес логики. В большинстве типизированных функционалых языках программирования предлагаются два класса для обработки ошибок и исключений: Try и Either.

Таким образом, можно переписать вышенаписанный запрос как следующий: Библиотека Arrow позволяет использовать эти абстракции вместе с Kotlin.

fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() }
}

Чем этот подход отличается от использования try-catch?

Тем более, что компайлер заругается, если это не будет сделано. Во-первых, любой кто будет читать этот код после тебя (а такие скорее всего будут) уже по сигнатуре сможет понять, что исполнение кода может привести к ошибке — и написать код её обработки.

Во-вторых, появляется гибкость в том, как ошибка может быть обработана.

Если мы хотим, чтобы функция всегда что-то возвращала при ошибке, можно задать дефолтное значение: Внутри Try ошибка или успех исполнения представлены в виде классов Failure и Success соответственно.

makeRequest(request).getOrElse { emptyList() }

Если требуется обработка ошибики посложнее, на помощь приходит fold:

makeRequest(request).fold( {ex -> // делаем что-то с ошибкой и возвращаем дефолтное значение emptyList() }, { data -> /* используем полученные данные */ }
)

Можно воспользоваться функцией recover — ее содержимое будет полностью проигнорировано, если Try вернет Success.

makeRequest(request).recover { emptyList() }

Можно исопользовать for comprehensions (позаимствованные создателями Arrow из Scala), если требуется обработка результата Success с помощью последовательности команд, путем вызова фабрики .monad() на Try:

Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result)
}

Вариант выше можно написать без использования binding, но тогда он будет по-другому читаться:

httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) }

В конце концов, результат функции можно обработать с помощью when:

when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message
}

Таким образом с помощью Arrow можно заменить далеко не идеальную конструкцию try-catch на что-то гибкое и очень удобное. Дополонительным плюсом использования Arrow является то, что не смотря на то, что библиотека позиционирует себя как функциональная — отдельные абстракции оттуда (например, тот же Try) можно использовать, продолжая писать старый добрый ООП код. Но предупреждаю — может понравиться и втянетесь, через пару недель начнете изучать Haskell, а ваши коллеги очень скоро перестанут понимать ваши рассуждения о структуре кода.

S.: Оно того стоит:) P.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть