arrow-kt / arrow

Λrrow - Functional companion to Kotlin's Standard Library
http://arrow-kt.io
Other
6.17k stars 451 forks source link

Either.isRight() contract's seems to not works #3476

Open mateuszjarzyna opened 2 months ago

mateuszjarzyna commented 2 months ago

I have extremely simple code, it's a function that returns Either<SomeError, CommandResult>

            val createdCourse: Either<OwlerError, CommandResult> =
                adminHub.createCourse(courseNameField(form), request.toRequestContext(instantSource.instant()))
            if (createdCourse.isRight()) {
                createdCourse.getOrNull() // it's still the either
            }

My case is that I liked to check if a course was created, but contract/smart cast seems to not work

The CommandResult is a pretty simple interface with two implementations

sealed interface CommandResult {

    val internalEvent: InternalEvent
    val publicEvents: List<PublicEvent>

    data class SuccessfulCommandResult(
        override val internalEvent: InternalEvent,
        override val publicEvents: List<PublicEvent>,
    ) : CommandResult

    data class EntityCreatedCommandResult<E>(
        override val internalEvent: InternalEvent,
        override val publicEvents: List<PublicEvent>,
        val createdEntity: E,
    ) : CommandResult
}

And OwlerError is another interface

sealed interface OwlerError {
    interface DomainError : OwlerError

    data class ValidationError(val errors: NonEmptySet<FieldErrors>) : OwlerError {
        companion object {
            fun from(violations: Set<ConstraintViolation>): ValidationError {
                return ValidationError(violations.mapErrors())
            }
        }
    }
}

I use arrow-kt in version "1.2.4"

And the screenshot from my IDE showing that smart casting does not work corretly.

Zrzut ekranu 2024-07-20 o 14 09 38

If it is relevant here is full list of dependencies

val http4kVersion: String by project
val http4kConnectVersion: String by project
val junitVersion: String by project
val kotlinVersion: String by project
val kotestVersion: String = "5.9.1"
val arrowVersion: String = "1.2.4"
val akkurateVersion: String = "0.8.0"

dependencies {
    implementation(platform("io.arrow-kt:arrow-stack:${arrowVersion}"))

    implementation("org.http4k:http4k-client-okhttp:${http4kVersion}")
    implementation("org.http4k:http4k-connect-ai-openai:${http4kConnectVersion}")
    implementation("org.http4k:http4k-contract:${http4kVersion}")
    implementation("org.http4k:http4k-core:${http4kVersion}")
    implementation("org.http4k:http4k-format-jackson:${http4kVersion}")
    implementation("org.http4k:http4k-opentelemetry:${http4kVersion}")
    implementation("org.http4k:http4k-template-handlebars:${http4kVersion}")
    implementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
    implementation("dev.nesk.akkurate:akkurate-core:${akkurateVersion}")
    implementation("dev.nesk.akkurate:akkurate-ksp-plugin:${akkurateVersion}")
    implementation("dev.nesk.akkurate:akkurate-arrow:${akkurateVersion}")
    implementation("io.arrow-kt:arrow-core")
    implementation("io.arrow-kt:arrow-fx-coroutines")
    implementation("io.kotest.extensions:kotest-assertions-arrow:1.4.0")
    implementation("com.github.ksuid:ksuid:1.1.2")
    implementation("com.github.slugify:slugify:3.0.7")

    ksp("dev.nesk.akkurate:akkurate-ksp-plugin:${akkurateVersion}")

    testImplementation("org.http4k:http4k-connect-ai-openai-fake:${http4kConnectVersion}")
    testImplementation("org.http4k:http4k-testing-approval:${http4kVersion}")
    testImplementation("org.http4k:http4k-testing-chaos:${http4kVersion}")
    testImplementation("org.http4k:http4k-testing-hamkrest:${http4kVersion}")
    testImplementation("org.http4k:http4k-testing-kotest:${http4kVersion}")
    testImplementation("org.http4k:http4k-testing-tracerbullet:${http4kVersion}")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2")
    testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2")
    testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
    testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
    testImplementation("io.kotest:kotest-property:$kotestVersion")
    testImplementation("io.kotest:kotest-framework-datatest:$kotestVersion")
}
mateuszjarzyna commented 2 months ago

I've created isRight2 function

@OptIn(ExperimentalContracts::class)
private fun  <A, B>  Either<A, B>.isRight2(): Boolean {
    contract {
        returns(true) implies (this@isRight2 is Right<B>)
        returns(false) implies (this@isRight2 is Left<A>)
    }
    return this@isRight2 is Right<B>
}

that works as expected.

Zrzut ekranu 2024-07-20 o 14 23 28 Zrzut ekranu 2024-07-20 o 14 23 39

So it seems like there is some kind of bug in smart casting with isRight/isLeft methods

serras commented 1 month ago

This is a known problem in Kotlin 1.x, where smart casting does not always work with generic types like Either.

@mateuszjarzyna Does this problem also reproduce with Kotlin 2.0?