Функциональный подход к обработке ошибок заключается в том, чтобы при проектировании не тотальных* функций, для сигнализации об ошибках использовать не исключения, а тип-контейнер в качестве результата функции, который может содержать в себе либо результат успешного выполнения, либо ошибку.
* функция является тотальной в только том случае, если она всегда для любых значений своих аргументов и любого состояния глобального окружения может вернуть корректный результат. Примером тотальной функции является функция возведения в квадрат значения типа BigInteger. А примерами не тотальных функций являются:
|
Таким типом может служить либо kotlin.Result
из стандартной библиотеки, либо какой-то монадический тип из сторонней библиотеки (такие как Either из Arrow или Result из kotlin-result), либо специфичная для функции закрытая (sealed) иерархия типов.
Специфическим для Kotlin-а (и других языков с null безопасностью) является возврат null для сигнализации об ошибке. Но этот способ можно использовать только в том случае, если в функции может случиться только одна ошибка, обработка которой не требует какой-то дополнительной информации.
Соответственно, при обнаружении ошибки, функция вместо выброса исключения, возвращает вариант результирующего значения сигнализирующий об ошибке.
Примеры
Кастомный ADT
Условно официально рекомендуемый стиль обработки доменных ошибок в Kotlin.
sealed interface ParseResult {
@JvmInline
value class Success(val value: Int) : ParseResult
data class Failure(val pos: Int, val char: Char) : ParseResult
}
fun parseInt(str: String): ParseResult {
var res = 0
var sign = 1
str.forEachIndexed { idx, char ->
when (char) {
'-' if idx == 0 -> sign = -1
in '0'..'9' -> res = res * 10 + char.digitToInt()
else -> return ParseResult.Failure(idx, char)
}
}
return ParseResult.Success(res * sign)
}
Result из стандартной библиотеки
Рекомендуемый Эргономичным подходом вариант реализации функционального стиля обработки ошибок.
Он требует меньше церемоний, чем вариант с ADT (не надо заводить корневой класс иерархии и вариант для успешного результата) и сразу даёт вспомогательные функции работы со значением в контейнере в духе map
/recover
.
Кроме того, вопреки вышеупомянотму посту и разделу Error-handling style and exceptions в KEEP-е посвящённому дизайну Result
-а, в том же KEEP-е функциональный стиль обработки ошибок упоминается как один из целевых юз кейсов этого типа.
Однако при выборе этого варианта стоит помнить о том, что создание экземпляра исключения - это дорогая операция, и в "горячем" коде лучше выбрать один из альтернативных вариантов.
data class UnparseableString(
val idx: Int,
val char: Char
) : RuntimeException()
fun parseInt(str: String): Result<Int> {
var res = 0
var sign = 1
str.forEachIndexed { idx, char ->
when (char) {
'-' if idx == 0 -> sign = -1
in '0'..'9' -> res = res * 10 + char.digitToInt()
else -> return failure(UnparseableString(idx, char))
}
}
return success(res * sign)
}
Result из сторонней библиотеки
Result из стандартной библиотеки хорош только тем, что он из стандартной библиотеки. И тем кто не боится добавлять зависимости в проект лучше взять одну из сторонних библиотек, предлагающих полноценный монадический тип с более богатой библиотекой функций работы со значениями в контейнере.
data class UnparseableString(
val idx: Int,
val char: Char
)
fun parseInt(str: String): Result<Int, UnparseableString> {
var res = 0
var sign = 1
str.forEachIndexed { idx, char ->
when (char) {
'-' if idx == 0 -> sign = -1
in '0'..'9' -> res = res * 10 + char.digitToInt()
else -> return Err(UnparseableString(idx, char))
}
}
return Ok(res * sign)
}
Нуллабельный тип результата функции
Возврат null для сигнализации об ошибке - де-факто официально рекомендуемый (см. функции *OrNull в стандартной библиотеке) стиль обработки ошибок, в случаях когда обработка не требует какой-то дополнительной информации.
fun parseInt(str: String): Int? {
var res = 0
var sign = 1
str.forEachIndexed { idx, char ->
when (char) {
'-' if idx == 0 -> sign = -1
in '0'..'9' -> res = res * 10 + char.digitToInt()
else -> return null
}
}
return res * sign
}