Статьи

Kotlin — попробуйте тип для функциональной обработки исключений

Scala имеет тип Try для функциональной обработки исключений. Я мог бы получить голову, используя этот тип, используя отличный
Путеводитель Неофита по Скале от Дэниела Вестхайде . Этот пост будет копировать этот тип, используя
Котлин .

Фон

Рассмотрим простую функцию, которая принимает две строки, преобразует их в целое число, а затем делит их (пример на основе скалярной
Попробуй ):

1
2
3
4
5
fun divide(dividend: String, divisor: String): Int {
     val num = dividend.toInt()
     val denom = divisor.toInt()
     return num / denom
 }

Ответственность за то, чтобы все исключения, распространяемые из этой реализации, обрабатывались надлежащим образом с использованием механизма обработки исключений Java / Kotlin:

1
2
3
4
5
6
7
try {
    divide("5t", "4")
} catch (e: ArithmeticException) {
    println("Got an exception $e")
} catch (e: NumberFormatException) {
    println("Got an exception $e")
}

Моя цель с кодом «Try» будет состоять в том, чтобы преобразовать «деление» во что-то похожее на это:

1
2
3
4
5
fun divideFn(dividend: String, divisor: String): Try<Int> {
    val num = Try { dividend.toInt() }
    val denom = Try { divisor.toInt() }
    return num.flatMap { n -> denom.map { d -> n / d } }
}

Вызывающая сторона этого варианта функции «деления» не будет иметь исключение для обработки через блок try / catch, вместо этого она получит исключение в виде значения, которое оно может проанализировать и при необходимости действовать.

1
2
3
4
5
val result = divideFn("5t", "4")
when(result) {
    is Success -> println("Got ${result.value}")
    is Failure -> println("An error : ${result.e}")
}

Котлинская реализация

Тип «Try» имеет две реализации, соответствующие пути «Success» или «Failure», и реализованы как запечатанный класс следующим образом:

1
2
3
4
5
sealed class Try<out T> {}
 
data class Success<out T>(val value: T) : Try<T>() {}
 
data class Failure<out T>(val e: Throwable) : Try<T>() {}

Тип «Success» охватывает успешный результат выполнения, а тип «Failure» — любое исключение, выбрасываемое из выполнения.

Итак, теперь, чтобы добавить немного мяса к этому, мой первый тест должен вернуть один из этих типов, основанный на чистой и исключительной реализации, по следующим направлениям:

01
02
03
04
05
06
07
08
09
10
val trySuccessResult: Try<Int> = Try {
    4 / 2
}
assertThat(trySuccessResult.isSuccess()).isTrue()
 
 
val tryFailureResult: Try<Int> = Try {
    1 / 0
}
assertThat(tryFailureResult.isFailure()).isTrue()

Этого можно достичь с помощью «объекта-компаньона» в Kotlin, аналогичного статическим методам в Java, который возвращает либо тип Success, либо тип Failure, основанный на выполнении лямбда-выражения:

01
02
03
04
05
06
07
08
09
10
11
12
13
sealed class Try<out T> {
    ...   
    companion object {
        operator fun <T> invoke(body: () -> T): Try<T> {
            return try {
                Success(body())
            } catch (e: Exception) {
                Failure(e)
            }
        }
    }
    ...
}

Теперь, когда вызывающая сторона имеет тип «Try», они могут проверить, является ли это типом «Success» или «Failure», используя выражение «when», как раньше, или используя методы «isSuccess» и «isFailure», которые делегированы для подтипов, как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
sealed class Try<out T> {
    abstract fun isSuccess(): Boolean
    abstract fun isFailure(): Boolean
}
 
data class Success<out T>(val value: T) : Try<T>() {
    override fun isSuccess(): Boolean = true
    override fun isFailure(): Boolean = false
}
 
data class Failure<out T>(val e: Throwable) : Try<T>() {
    override fun isSuccess(): Boolean = false
    override fun isFailure(): Boolean = true
}

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

1
2
3
4
5
6
7
8
9
val t1 = Try { 1 }
 
assertThat(t1.getOrElse(100)).isEqualTo(1)
 
val t2 = Try { "something" }
        .map { it.toInt() }
        .getOrElse(100)
 
assertThat(t2).isEqualTo(100)

снова реализуется путем делегирования подтипам:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
sealed class Try<out T> {
  abstract fun get(): T
  abstract fun getOrElse(default: @UnsafeVariance T): T
  abstract fun orElse(default: Try<@UnsafeVariance T>): Try<T>
}
 
data class Success<out T>(val value: T) : Try<T>() {
    override fun getOrElse(default: @UnsafeVariance T): T = value
    override fun get() = value
    override fun orElse(default: Try<@UnsafeVariance T>): Try<T> = this
}
 
data class Failure<out T>(val e: Throwable) : Try<T>() {
    override fun getOrElse(default: @UnsafeVariance T): T = default
    override fun get(): T = throw e
    override fun orElse(default: Try<@UnsafeVariance T>): Try<T> = default
}

Однако самое большое преимущество возврата типа «Try» заключается в цепочке дальнейших операций над этим типом.

Сцепление с картой и flatMap

Операции «map» передается лямбда-выражение для преобразования значения в какой-либо форме — возможно, даже в другой тип:

1
2
3
4
5
val t1 = Try { 2 }
 
val t2 = t1.map({ it * 2 }).map { it.toString()}
 
assertThat(t2).isEqualTo(Success("4"))

Здесь число удваивается, а затем преобразуется в строку. Если начальная попытка была «Отказ», то окончательное значение просто вернет «Отказ» в соответствии с этим тестом:

1
2
3
4
5
6
7
val t1 = Try {
    2 / 0
}
 
val t2 = t1.map({ it * 2 }).map { it * it }
 
assertThat(t2).isEqualTo(Failure<Int>((t2 as Failure).e))

Реализация «карты» довольно проста:

01
02
03
04
05
06
07
08
09
10
sealed class Try<out T> {
    fun <U> map(f: (T) -> U): Try<U> {
        return when (this) {
            is Success -> Try {
                f(this.value)
            }
            is Failure -> this as Failure<U>
        }
    }
}

flatmap, с другой стороны, принимает лямбда-выражение, которое возвращает другой тип «Try» и сглаживает результат обратно в тип «Try», в соответствии со строками этого теста:

1
2
3
4
5
6
7
val t1 = Try { 2 }
 
val t2 = t1
        .flatMap { i -> Try { i * 2 } }
        .flatMap { i -> Try { i.toString() } }
 
assertThat(t2).isEqualTo(Success("4"))

Реализовать это тоже просто, по следующим направлениям:

1
2
3
4
5
6
7
8
sealed class Try<out T> {
    fun <U> flatMap(f: (T) -> Try<U>): Try<U> {
        return when (this) {
            is Success -> f(this.value)
            is Failure -> this as Failure<U>
        }
    }
}

Методы «map» и «flatMap» являются мощными инструментами этого типа, позволяющими объединять в цепочку сложные операции, ориентируясь на счастливый путь.

Вывод

Try — это мощный тип, позволяющий функционально обрабатывать исключения в коде. У меня есть соломенная реализация с использованием Kotlin, доступная в моем репозитории github здесь — https://github.com/bijukunjummen/kfun

Смотрите оригинальную статью здесь: Kotlin — Попробуйте тип для функциональной обработки исключений

Мнения, высказанные участниками Java Code Geeks, являются их собственными.