- Type: Design proposal
- Author: Viacheslav Makarov
- Status: Draft
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 makethis
fully invariant, and set type parameter ofthis
toT2
.'Detailed design' will likely be rewritten.
As a result, a variance-unsafe statement will not pass type checking.
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.
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.
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.
At the moment:
-
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 usethis
as ifQuestion1
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) }
-
Every invariant type, when
out
-projected, prohibits use of its contravariant methods. Its properties types are alsoout
-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.
Proposed rules are:
- Non-
private
method or a property declaration is allowed to have anout
type parameter inin
or invariant position – non-private non-covariant declaration. - When type checking an expression within the body of a non-private non-covariant declaration,
all private declarations accessible from
this
must have their signatureout
-projected. - The backing field of a property with non-covariant type should be seen as private non-covariant declaration.
- Non-private non-covariant declaration must be
final
. It must not beopen
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
}
- 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.
We will glad to see this proposal as a pull request into https://github.com/Kotlin/KEEP