#kotlin #functional-programming #arrow-kt
#kotlin #функциональное программирование #стрелка-kt
Вопрос:
У меня есть класс A
:
class A (private var z: String, private var y: String, private var x: Int)
Я хочу создать для него отказоустойчивый конструктор. Конструктор должен возвращать Either
список исключений (например, когда значения отсутствуют) или созданные значения. Каков рекомендуемый способ создать что-то подобное?Или есть концептуально лучший подход?
Мой собственный подход к этому:
sealed class ABuilderException {
object MissingXValue : ABuilderException()
object MissingYValue : ABuilderException()
object MissingZValue : ABuilderException()
}
import arrow.core.Either
import arrow.core.Option
import arrow.core.none
import arrow.core.some
class ABuilder {
private var x : Option<Int> = none()
private var y : Option<String> = none()
private var z : Option<String> = none()
fun withX(x : Int) : ABuilder {
this.x = x.some();
return this;
}
fun withY(y : String) : ABuilder {
this.y = y.some();
return this;
}
fun withZ(z : String) : ABuilder {
this.z = z.some();
return this;
}
fun build() : Either<A, List<ABuilderException>> {
var xEither = x.toEither { ABuilderException.MissingXValue }
var yEither = y.toEither { ABuilderException.MissingYValue }
var zEither = z.toEither { ABuilderException.MissingZValue }
// If all values are not an exception, create A
// otherwise: Return the list of exceptions
}
}
Как я мог бы наилучшим образом завершить build
код?
Я предпочитаю решение, которое позволяет избежать глубокой вложенности (например orElse
, или аналогичных методов) и избегает повторения значений (например, путем воссоздания кортежей), поскольку это может привести к опечаткам и затруднит добавление / удаление свойств позже.
Ответ №1:
Сначала вам нужно изменить подпись build
на:
fun build() : Either<List<ABuilderException>, A>
Причина этого Either
заключается в том, что это правильное смещение — такие функции, как map
, flatMap
и т. Д., Работают со Right
значением и не работают, если значение равно Left
.
Для объединения Either
значений вы можете использовать zip
:
val e1 = 2.right()
val e2 = 3.right()
// By default it gives you a `Pair` of the two
val c1 = e1.zip(e2) // Either.Right((2, 3))
// Or you can pass a custom combine function
val c2 = e1.zip(e2) { two, three -> two three } // Either.Right(5)
Однако здесь есть проблема, в случае ошибки (одна из них Left
) она быстро завершится ошибкой и выдаст вам только первую.
Для накопления ошибок мы можем использовать Validated
:
val x = none<Int>()
val y = none<String>()
val z = none<String>()
// Validated<String, Int>
val xa = Validated.fromOption(x) { "X is missing" }
// Validated<String, String>
val ya = Validated.fromOption(y) { "Y is missing" }
// Validated<String, String>
val za = Validated.fromOption(z) { "Z is missing" }
xa.toValidatedNel().zip(
ya.toValidatedNel(),
za.toValidatedNel()
) { x, y, z -> TODO() }
Validated
, like Either
имеет zip
функцию для объединения значений. Разница в том, что Validated
ошибки будут накапливаться. В лямбда-выражении у вас есть доступ к допустимым значениям ( Int
, String
, String
), и вы можете создать свой допустимый объект.
toValidatedNel()
здесь преобразуется из Validated<String, String>
в Validated<Nel<String>, String>
where Nel
— список, который НЕ может быть пустым. Накопление ошибок как a List
является обычным явлением, поэтому оно встроено.
Для получения дополнительной информации вы можете ознакомиться с руководством по обработке ошибок в документации.
Комментарии:
1. Спасибо, это решило проблему. Ваши объяснения были очень полезными и познавательными!