```when (val op = MyAnnotationOperand.from(declara...
# getting-started
m
Copy code
when (val op = MyAnnotationOperand.from(declaration.symbol, context.session)) {
    is Either.Error<MyAnnotationOperand.FactoryError.NotAnnotatedClass> -> TODO()
    is Either.Error<MyAnnotationOperand.FactoryError.MissingArgument> -> TODO()
    is Either.Error<MyAnnotationOperand.FactoryError.InvalidArgument> -> TODO()
    is Either.Happy<MyAnnotationOperand> -> TODO()
}
How can I make this work?
Copy code
sealed class Either<out Y, out N> {
    class Happy<out Y>(val happy: Y): Either<Y, Nothing>()
    class Error<out N>(val error: N): Either<Nothing, N>()
}
This is the
Either
type in use.
Copy code
inline fun <T: Any> T?.coalesce(fallback: () -> T): T {
    return this ?: fallback()
}

private class MyAnnotationOperand private constructor(
    val annotatedClass: FirClassSymbol<*>,
    val annotationArgument: ClassId,
) {
    sealed class FactoryError {
        object NotAnnotatedClass: FactoryError()
        object MissingArgument: FactoryError()
        object InvalidArgument: FactoryError()
    }

    companion object {
        fun from(
            maybeAnnotatedClass: FirClassSymbol<*>,
            session: FirSession,
        ): Either<MyAnnotationOperand, FactoryError> {
            return Either.Happy(MyAnnotationOperand(
                fencedClass = maybeAnnotatedClass,
                fenceArgument = maybeAnnotatedClass
                    .getAnnotationByClassId(
                        classId = ANNOTATION_CLASS,
                        session = session,
                    )
                    .coalesce { return Either.Error(FactoryError.NotAnnotatedClass) }
                    .getKClassArgument(ANNOTATION_ARG)
                    .coalesce { return Either.Error(FactoryError.MissingArgument) }
                    .classId
                    .coalesce { return Either.Error(FactoryError.InvalidArgument) }
            ))
        }
    }
}
And this is
MyAnnotationOperand.from
y
Copy code
when (val op = MyAnnotationOperand.from(declaration.symbol, context.session)) {
    is Either.Error -> when (op.error) {
        MyAnnotationOperand.FactoryError.NotAnnotatedClass -> TODO()
        MyAnnotationOperand.FactoryError.MissingArgument -> TODO()
        MyAnnotationOperand.FactoryError.InvalidArgument -> TODO()
    }
    is Either.Happy -> TODO()
}
This would get better if we have Rich Errors
m
Why don’t they just improve
when
so the nesting is unnecessary?
Thank you for the solution though!
y
The issue is more fundamental than that. Generics are "erased" in Kotlin (because they are on the JVM). This means that a check like
is Foo<...>
cannot be done in general. Your case specifically allows for that, sure, but it's still unclear how the compiler would know to look at
error
or
happy
to check the type. Some improved
when
could do the trick, sure, but that hasn't been designed yet. If
when
had some kind of pattern matching capability, then you could do:
Copy code
// imaginary syntax
when (val op = MyAnnotationOperand.from(declaration.symbol, context.session)) {
    matches Either.Error(error is MyAnnotationOperand.FactoryError.NotAnnotatedClass) -> TODO()
    matches Either.Error(error is MyAnnotationOperand.FactoryError.MissingArgument) -> TODO()
    matches Either.Error(error is MyAnnotationOperand.FactoryError.InvalidArgument) -> TODO()
    is Either.Happy -> TODO()
}
but again, a fleshed out design for pattern matching hasn't happened yet, although gradual improvements are happening (e.g. when guards) FWIW, there is an alternative solution using contracts that I forgot to mention:
Copy code
import kotlin.contracts.*

// `N: Any` to avoid `Either.Error(null)` being confused with `Either.Happy`
fun <Y, N: Any> Either<Y, N>.errorOrNull(): N? {
    contract {
        returns(null) implies (this@errorOrNull is Either.Happy)
        returnsNotNull() implies (this@errorOrNull is Either.Error)
    }
    return when (this) {
        is Either.Happy -> null
        is Either.Error -> error
    }
}

fun main() {
    val op = MyAnnotationOperand.from()
    when (op.errorOrNull()) {
        is MyAnnotationOperand.FactoryError.NotAnnotatedClass -> TODO()
        is MyAnnotationOperand.FactoryError.MissingArgument -> TODO()
        is MyAnnotationOperand.FactoryError.InvalidArgument -> TODO()
        null -> op.happy // smart cast to Either.Happy
    }
}
you still get the exhaustivity for errors, and the happy path gets a smart cast too.
m
I’ve been told that the following is supposed to work:
Copy code
sealed class Either<out L, out R> private constructor() {
    class Foo<out L>(val foo: L): Either<L, Nothing>()
    class Bar<out R>(val bar: R): Either<Nothing, R>()
}

sealed class Choice private constructor() {
    class Case0(val int: Int): Choice()
    class Case1(val str: String): Choice()
}

fun main() {
    for (either in listOf<Either<Boolean, Choice>>(
    	Either.Foo(true),
        Either.Bar(Choice.Case0(42)),
        Either.Bar(Choice.Case1("")),
    )) {
        println(when (either) {
            is Either.Foo -> "Either.Foo(${ either.foo })"
            is Either.Bar if either.bar is Choice.Case0
            	-> "Either.Bar(Choice.Case0(${ either.bar.int }))"
            is Either.Bar if either.bar is Choice.Case1
            	-> "Either.Bar(Choice.Caes1(${ either.bar.str }))"
        })
    }
}
But I get the following compiler error:
Copy code
'when' expression must be exhaustive. Add the 'is Bar<*>' branch or an 'else' branch.
Are you aware of a way around this?
y
Sadly, when guards don't support exhaustivity, hence the error I'd instead use a nested
when
, or the
errorOrNull
pattern with contracts mentioned above