Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@optics generates bad code when dealing with generic properties in a sealed hierarchy #3381

Closed
sindrenm opened this issue Feb 27, 2024 · 0 comments · Fixed by #3382
Closed

Comments

@sindrenm
Copy link

@optics generates bad code for sealed hierarchies with generic properties.

This is a new issue in arrow-optics 1.2.2 that wasn't present in 1.1.5.

I'm not entirely sure if I consider this an Arrow issue or a Kotlin compiler issue, since I believe it might be possible for the compiler to infer the correct types in these examples in particular. (We're using Kotlin 1.9.22 in this case.)

The following code will generate Kotlin code that doesn't compile:

@optics
sealed interface Base<out T> {
    val prop: T

    companion object
}

@optics
data class Child1(override val prop: String) : Base<String> {
    companion object
}

@optics
data class Child2(override val prop: Int) : Base<Int> {
    companion object
}

The generated code looks like this:

inline fun <T> Base.Companion.prop(): Lens<Base<T>, T> =
    Lens(
        get = { base: Base<T> -> base.`prop` },
        set = { base: Base<T>, value: T ->
            // Type mismatch: required return value value to be Base<T>, was Base<Any>
            when (base) {
                // Type mismatch: required value to be String, was T
                is Child1 -> base.copy(`prop` = value)

                // Type mismatch: required value to be Int, was T
                is Child2 -> base.copy(`prop` = value)
            }
        },
    )

This code could be made to compile by adding some type casting:

inline fun <T> Base.Companion.prop(): Lens<Base<T>, T> =
    Lens(
        get = { base: Base<T> -> base.`prop` },
        set = { base: Base<T>, value: T ->
            when (base) {
                is Child1 -> base.copy(`prop` = value as String) as Base<T>
                is Child2 -> base.copy(`prop` = value as Int) as Base<T>
            }
        },
    )

I'm not, however, sure how safe those casts are. Some rudimentary tests seem to suggest it's fine, but I haven't spent much time thinking about it:

fun main() {
    val child1 = Child1("hello")
    val child2 = Child2(1)

    println(Base.prop<String>().get(child1)) // compiles
    println(Base.prop<Int>().get(child1))    // does not compile

    println(Base.prop<Int>().get(child2))    // compiles
    println(Base.prop<String>().get(child2)) // does not compile
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant