Skip to content

Instantly share code, notes, and snippets.

@mekarthedev
Last active September 9, 2023 21:51
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mekarthedev/cd2a3a06691db70414c2a764a9d4993b to your computer and use it in GitHub Desktop.
Save mekarthedev/cd2a3a06691db70414c2a764a9d4993b to your computer and use it in GitHub Desktop.
Kotlin proposal: Allow `out` type parameter to occur in `in` position

Allow out type parameter to safely occur in in position

  • Type: Design proposal
  • Author: Viacheslav Makarov
  • Status: Draft

Summary

Reduce the need in @UnsafeVariance. Allow covariant types to have non-private methods with non-covariant signature. To protect class' covariance, out-project signatures of all private declarations accessible from this, while keeping signatures of non-private declarations unchanged.

// TBD

Instead of out-projecting privates, implicitly declare T2: T, temporarily make this fully invariant, and set type parameter of this to T2.

'Detailed design' will likely be rewritten.

As a result, a variance-unsafe statement will not pass type checking.

Motivation

The following Kotlin code will not compile.

class Question1<out Answer>(private val rightAnswer: Answer) {
    // Error: Type parameter Answer is declared as 'out' but occurs in 'in' position
    fun canBeAnsweredWith(proposedAnswer: Answer /* <- Error */): Boolean {
        return rightAnswer == proposedAnswer
    }
}

Compiler is complaining here about possible Question1's covariance violation. But compiler is obviously missing the point. There's no conceptual issue with variance here. Indeed, a Question1<Integer> instance might be upcasted to Question1<Any?> allowing canBeAnsweredWith to be called with a String argument. But it's not going to break the actual instance's internal consistency.

An easy workaround is to extract canBeAnsweredWith as an extension function.

class Question2<out Answer>(val rightAnswer: Answer)

fun <Answer> Question2<Answer>.canBeAnsweredWith(proposedAnswer: Answer): Boolean {
    return rightAnswer == proposedAnswer
}

But there is a consequence that rightAnswer have to be accessible outside Question2.

Exposing private API makes code unsafe. More importantly, it makes this code less expressive in terms of modeling domain logic. In the above example private-ness of the right answer might be fundamental for the modeled domain.

@UnsafeVariance

Another workaround is to use @UnsafeVariance annotation which simply suppresses the error. With @UnsafeVariance the author of a class have to take full responsibility for correct handling of the class' variance.

Just like in those old days when developer was responsible for making sure a reference is not null before member access. Except that the only possible outcome was NullPointerException. And with broken variance there might be various consequences.

Java interop

Libraries written in Kotlin might be easily used in Java projects. Library author might want to design library in such way that it will be comfortable to use it from Java code.

Calling Kotlin's extension functions from Java is not so convenient. The only alternative is to declare these functions as member functions of related classes. And if some class is covariant or contravariant the problem of conflicting variance may arise.

If ease of use is a priority, the only option left is @UnsafeVariance. With all consequences of unsafe programming.

Another use case is gradual conversion of large Java code base to Kotlin. For example, it could be imagined that popular library RxJava is some day going to be converted to Kotlin.

To make it gradualy, one may start with basic things like Observable class. Observable is effectively covariant. But having it alone converted to Kotlin, and declaring its type parameter out forces developer to mark tons of its methods with @UnsafeVariance.

Otherwise these methods have to be (and wanted to be) moved to extension functions. But it would mean updating half of a typical RxJava-based Java application. Not to mention tons of updates within RxJava itself.

Description

At the moment:

  1. Every covariant type is able to declare and use its own private contravariant or invariant methods and properties. In the following example the code within isAnswerable is allowed to use this as if Question1 was invariant.

    class Question1<out Answer>(private val rightAnswer: Answer) {
        private fun canBeAnsweredWith(proposedAnswer: Answer): Boolean {
            return rightAnswer == proposedAnswer
        }
        val isAnswerable: Boolean get() = canBeAnsweredWith(rightAnswer)
    }
  2. Every invariant type, when out-projected, prohibits use of its contravariant methods. Its properties types are also out-projected making some property's type methods effectively unusable.

The idea of this proposal is to mix these features. This should allow a covariant type to have public methods with non-covariant signature, but make this of these methods partially out-projected. "Partially" means that out-projection should take effect only on private declarations accessible from this.

Unlike extension function, such method is allowed to use any private API as long as this API is variance-safe, i.e. may not break type's covariance.

Detailed design

Proposed rules are:

  1. Non-private method or a property declaration is allowed to have an out type parameter in in or invariant position – non-private non-covariant declaration.
  2. When type checking an expression within the body of a non-private non-covariant declaration, all private declarations accessible from this must have their signature out-projected.
  3. The backing field of a property with non-covariant type should be seen as private non-covariant declaration.
  4. Non-private non-covariant declaration must be final. It must not be open or be a default interface method implementation.

For example, if a covariant by T class has public and private methods with arguments of type T, then a public one is not able to pass T as an argument to a private one, yet being able to pass it to another public method.

Expected behavior:

open class X<out T>(
        private val value: T,
        private val output: (T) -> T,
        val process: (T) -> Unit  // Error: type in 'in' position cannot be less restrictive than Nothing.
) {
    private fun read(): T = this.value
    private fun handle(t: T) {}

    var helper: (T) -> T = { it }  // OK
        get() = field  // Error: type mismatch: required (T) -> T, found (Nothing) -> Nothing
        set(f) { field = f }  // Error: type mismatch: required (Nothing) -> Nothing, found (T) -> T

    var calculated: T = value  // OK
        set(t) { field = t }  // Error: type mismatch: required Nothing, found T

    fun check(t: T): T {  // OK

        this.handle(t)  // Error: type mismatch: required Nothing, found T

        val out = this.output  // OK, inferred (Nothing) -> Nothing
        out.invoke(t)  // Error: type mismatch: required Nothing, found T

        val help = this.helper  // OK, inferred (T) -> T
        val v = this.read()  // OK, inferred T
        return help.invoke(v)  // OK
    }

    open fun abs(t: T) {}  // Error: 'out' type parameter in 'in' position is not allowed for open fun
}

interface Y<out T> {
    fun f(t: T)  // Error: 'out' type parameter in 'in' position is not allowed for interface fun
}

TBD

  • How can NND read a private property and pass its value to a private method?
  • Are non-covariant methods really can't be open?
  • How Java interop is affected?
  • Investiage if something similar can be done for covariance-within-contravariance.
@abreslav
Copy link

abreslav commented May 3, 2018

Thanks for a very interesting proposal. We created a YT issue to track it here: https://youtrack.jetbrains.com/issue/KT-24214

@erokhins
Copy link

erokhins commented May 7, 2018

We will glad to see this proposal as a pull request into https://github.com/Kotlin/KEEP

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment