Создание конструктора объектов с обработкой ошибок с использованием сопоставления шаблонов со стрелками с несколькими Eithers

#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. Спасибо, это решило проблему. Ваши объяснения были очень полезными и познавательными!