Search code examples
kotlinfunctional-programmingvavr

How to flatMap vavr Either with left variance annotated


My code

open class Fail(override val message: String, override val cause: Throwable?) : RuntimeException(message, cause)

data class ValidationFail(override val message: String, override val cause: Throwable?) : Fail(message, cause)

more fails will be defined there in the future

i have 2 functions

fun fun1(): Either<out Fail, A>
fun fun2(a: A): Either<out Fail, B>

when i try to invoke them like this fun1().flatMap{fun2(it)} i got

Type mismatch: inferred type is (A!) -> Either<out Fail, B> but ((A!) -> Nothing)! was expected. Projected type Either<out Fail, A> restricts use of public final fun <U : Any!> flatMap(p0: ((R!) -> Either<L!, out U!>!)!): Either<L!, U!>! defined in io.vavr.control.Either

Code from vavr Either:

default <U> Either<L, U> flatMap(Function<? super R, ? extends Either<L, ? extends U>> mapper) {
    Objects.requireNonNull(mapper, "mapper is null");
    if (isRight()) {
        return (Either<L, U>) mapper.apply(get());
    } else {
        return (Either<L, U>) this;
    }
}

I guess o have this error because there is L in flatMap definition not ? extends L

Any workaround for this ?


Solution

  • In your particular case, you can make it compile by removing out variance from fun1 and fun2 return type. You shouldn't use wildcard types as return types anyway.

    But it won't help if you have fun1 and fun2 defined this way:

    fun fun1(): Either<ConcreteFail1, A>
    fun fun2(a: A): Either<ConcreteFail2, B>
    

    Replacing L with ? extends L in flatMap signature will not help either because of ConcreteFail2 not being a subtype of ConcreteFail1. The problem is that Either is supposed to be covariant, but there is no such thing as declaration-site variance in Java. Although there is a workaround using Either#narrow method:

    Either.narrow<Fail, A>(fun1()).flatMap { Either.narrow(fun2(it)) }
    

    Of course, it looks odd and must be extracted to a separate extension function:

    inline fun <L, R, R2> Either<out L, out R>.narrowedFlatMap(
        crossinline mapper: (R) -> Either<out L, out R2>
    ): Either<L, R2> = narrow.flatMap { mapper(it).narrow }
    

    Where narrow is:

    val <L, R> Either<out L, out R>.narrow: Either<L, R> get() = Either.narrow(this)
    

    I think Vavr doesn't provide its own narrowedFlatMap because this method requires using a wildcard receiver type, so it can't be a member method and must be a static one, which breaks all readability of operations pipelining:

    narrowedFlatMap(narrowedFlatMap(narrowedFlatMap(fun1()) { fun2(it) }) { fun3(it) }) { fun4(it) }
    

    But since we use Kotlin, we can pipeline static (extension) functions as well:

    fun1().narrowedFlatMap { fun2(it) }.narrowedFlatMap { fun3(it) }.narrowedFlatMap { fun4(it) }