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

Definitely non-nullable types #268

Open
dzharkov opened this issue Jul 19, 2021 · 35 comments
Open

Definitely non-nullable types #268

dzharkov opened this issue Jul 19, 2021 · 35 comments

Comments

@dzharkov
Copy link
Contributor

dzharkov commented Jul 19, 2021

The goal of this proposal is to allow explicitly declare definitely not-nullable type

First version (obsolete): https://github.com/Kotlin/KEEP/blob/7b998efaf70cc8d783a57af14b8701886089a5fe/proposals/definitely-not-nullable-types.md
Second version (11 Aug 2021): https://github.com/Kotlin/KEEP/blob/c72601cf35c1e95a541bb4b230edb474a6d1d1a8/proposals/definitely-non-nullable-types.md
Specific comments may be left here or at the #269

@BenWoodworth
Copy link

My first impression is that the syntax feels clunky, and is very specific. Could this be tackled with a more general solution?

Taking the original Java use-case:

<T> void put(@NotNull T t);

Could this be solved by introducing some sort of refined/non-parameter generic types?

fun <T> put(t: TNotNull) where TNotNull : T, TNotNull : Any
// Needs to be defined somehow ^^^^^^^^                 ^^^ "Type parameter cannot have any other bounds
//                                                           if it's bounded by another type parameter"

Or allowing @NotNull to be used the same way in Kotlin?

Using contracts also comes to mind, but that's a bit different semantically. It also (I think?) doesn't change the signature in the same way.

@altavir
Copy link

altavir commented Jul 19, 2021

I am not sure about the importance of the use case, but there is a problem with the syntax. Double bang (!!) is in most cases a synonym to code-smell and was introduced exactly for that. Using in for perfectly normal type is going to rise cognitive dissonance.

Also, have you considered a more universal way - type intersections. T & Any is longer, but it does not seem to be a very frequent use-case.

@dzharkov
Copy link
Contributor Author

@BenWoodworth Thanks for the suggestions!

My first impression is that the syntax feels clunky, and is very specific. Could this be tackled with a more general solution?

Yes, we may just introduce (at least partially) intersection types for T & Any

Could this be solved by introducing some sort of refined/non-parameter generic types?

fun <T> put(t: TNotNull) where TNotNull : T, TNotNull : Any
// Needs to be defined somehow ^^^^^^^^                 ^^^ "Type parameter cannot have any other bounds
//                                                           if it's bounded by another type parameter"

Such a type parameter would make a signature for put effectively different

Or allowing @NotNull to be used the same way in Kotlin?

We've always tried to avoid bringing Java-specific nullability annotations to Kotlin, since there's already a syntax for nullability.
Also, it would require having those annotations in stdlib or some other artifact shipped like a standard library.

@dzharkov
Copy link
Contributor Author

@altavir

I am not sure about the importance of the use case, but there is a problem with the syntax. Double bang (!!) is in most cases a synonym to code-smell and was introduced exactly for that. Using in for perfectly normal type is going to rise cognitive dissonance.

Thanks! That is a bit of dissonance we are afraid of. At the same time, one might have an intuition that it's not very surprising that expressions having a form of t!! are belonging to the type family T!!. But I see your concern.

@altavir
Copy link

altavir commented Jul 20, 2021

@dzharkov It is problematic from the teaching perspective. We are teaching that double-bang must be avoided and should be used only under very specific conditions. But introducing another similar syntax where recommendations are completely different, would bring a lot of confusion and significantly complicate the understanding. For new people, the similar syntax should have a similar meaning and similar usage practices if want it to keep being simple.

@fvasco
Copy link

fvasco commented Jul 20, 2021

I fully agree with @altavir.
I perceive the !! as a cast, it looks strange on a type.

var s: String?
println(s!!) // println(s as String)

Honestly, I like to understand the problem better, I don't feel it as a problem.

The example fun <T> elvisLike(x: T, y: T!!): T!!
can be already implemented as fun <T : Any> elvisLike(x: T?, y: T): T

@dzharkov
Copy link
Contributor Author

@fvasco elvisLike is not a use case, but it's more like an example of its semantics.
The only real use case we've got so far is the impossibility to override/implement some annotated Java API.

@quickstep24
Copy link

@altavir @fvasco This is not a contradiction to me. I would say "double-bang types" should also be avoided and only be used under very specific conditions. The KEEP states there are special conditions, where they are required for Java interoperability. The ugly double-bang would help guiding people to derive 'T' from non-nullable type and use 'T?' (as fvasco pointed out).

@ilya-g
Copy link
Member

ilya-g commented Jul 20, 2021

If this proposal is implemented, we're going to relax some stdlib function signatures by removing T : Any constraint from them and using T!! (or T & Any) type instead. For example:
fun <T : Any> Sequence<T?>.filterNotNull(): Sequence<T>
would become
fun <T> Sequence<T>.filterNotNull(): Sequence<T!!>

@trevorhackman
Copy link

Is there currently any difference at all between the following?

fun <T : CharSequence?> foo(t: T?, tn: T)

fun <T : CharSequence?> foo(t: T, tn: T?)

fun <T : CharSequence?> foo(t: T?, tn: T?)

fun <T : CharSequence?> foo(t: T, tn: T)

@quickstep24
Copy link

fun <T : CharSequence?> foo(t: T?, tn: T)

fun <T : CharSequence?> foo(t: T, tn: T?)

fun <T : CharSequence?> foo(t: T?, tn: T?)

fun <T : CharSequence?> foo(t: T, tn: T)

Yes, of course. Call foo<String>(null, "A") and you will see.

@antohaby
Copy link

antohaby commented Jul 26, 2021

If Kotlin had type intersections already, T & Any would make sense.
But it has not (yet ;)). And therefore for regular kotlin users this syntax would look obscure. So it did to me.
Of course Kotlin already have such intersections in inferred type hints so from this point of view it can worked out.

It would be much better to have just T! as a companion of T?. But as its mentioned in the KEEP its already occupied by flexible-types. But can't we find any other way to note it?

What if make T always 'definitely not-null' and therefore break BC, unfortunately.
But It would make Kotlin generic and non-generic type notations consistent.

// In both functions these statements would be correct
// [a] is definitely not nullable
// [b] is definitely not nullable
// [c] is nullable list of definitely not nullable elements

fun foo(a: Int, b: Int?, c: List<Int>?)
fun <T> bar(a: T, b: T?, c: List<T>?)

Flexible types would still work as currently. As far as I can see.

Second option is to change notation of flexible type T! to T!!. So T!! will now also express the "danger" of t!! in expressions. That using T as non-null is possible but dangerous. And T! will mean 'definitely not nullable'

What do you think? I did you discuss costs of breaking changes in this area?

@EddieRingle
Copy link

Or allowing @NotNull to be used the same way in Kotlin?

We've always tried to avoid bringing Java-specific nullability annotations to Kotlin, since there's already a syntax for nullability.
Also, it would require having those annotations in stdlib or some other artifact shipped like a standard library.

The only real use case we've got so far is the impossibility to override/implement some annotated Java API.

@dzharkov If there aren't any cases where this would be necessary outside of Java interop, it seems like it would be much simpler to add another JVM-specific annotation. I don't understand the concern of shipping it in the stdlib considering that the common namespace is already polluted with kotlin.jvm.* annotations.

@dzharkov dzharkov changed the title Definitely not-nullable types Definitely non-nullable types Aug 11, 2021
@dzharkov
Copy link
Contributor Author

Thank you all for your suggestions!
We've updated the proposal

The main change is that we moved from confusing T!! syntax to a limited version of intersection types – T & Any.
It's likely that at some point we'll have full support for intersection types and then it would be just a special case that doesn't deserve special syntax because it seems that the only real-world use-case we have by now is overrides of annotated Java.

On the concern that it might be obscure for newcomers:

  • It's unlikely that a lot of people would meet such types frequently considering it's rather rare
  • One still may google T & Any and find something about it and any other syntax will hardly be very clear (at least we haven't seen such options yet)

@dzharkov
Copy link
Contributor Author

Second option is to change notation of flexible type T! to T!!. So T!! will now also express the "danger" of t!! in expressions. That using T as non-null is possible but dangerous. And T! will mean 'definitely not nullable'

Changing notation for flexible types just because of a very rare feature looks too hard for me

@dzharkov
Copy link
Contributor Author

@dzharkov If there aren't any cases where this would be necessary outside of Java interop, it seems like it would be much simpler to add another JVM-specific annotation. I don't understand the concern of shipping it in the stdlib considering that the common namespace is already polluted with kotlin.jvm.* annotations.

Agree, one might say it's already polluted, but we still do our best to avoid making it worth (at least for rarely used features)

@kyay10
Copy link

kyay10 commented Aug 11, 2021

Is there a possibility maybe to put a (very rudimentary and rough) version of intersection types behind an experimental flag? Because IIRC intersection types are already in the compiler and so having a rough user-facing version of them behind a flag shouldn't be too difficult. Just a little something that we can experiment with for the time being

@NiematojakTomasz
Copy link

I'm definitely hyped for intersection types! Syntax sugar like T!! is something I don't care much about. Myself, I am not convinced we need T!! at all, as we can use : Any bound on T and just use T? where applicable. Seems more paradigmatic for me. I haven't seen so far use case for T!!, that would convince me that:
a) Is needed.
b) Is cleaner than puting : Any bound on T and using T? where applicable.
Except of interop issue mentioned in the proposal. (Unless we would always treat Java generic parameters as not null, and infer T? as type where not annotated @NotNull on annotated @Nullable) But I would prefer T & Any without support for syntax sugar, to avoid encouraging such constructs in plain Kotlin code.

@abreslav
Copy link
Contributor

For the record, while I understand the underpinnings of the particular syntax chosen (T & Any), I think it has a serious issue, i.e. it is not sufficiently self-explanatory. A person looking at it doesn’t immediately see the intent (“make T not-null”), but sees what appears to be a triviality (“Any is the top type, so T intersected with Any must be T”). Yes, the top type is Any?, but most users don’t realize this immediately. If we called the Any class something like “NotNullReference”, it would have been all different, but for better or worse we called it Any which is very misleading in this context. I would argue that even an annotation like @MakeNotNull, though obviously clunky and ad hoc, would serve better in this role.

@altavir
Copy link

altavir commented Sep 25, 2021

I agree, but is still better than T!! and plays well with hypothetic real intersection types in the future.

@roxton
Copy link

roxton commented Dec 31, 2021

Let me offer a use case that may motivate the ! as a complement to ? on types.

fun <T : Any> lookup(type: Class<T>) : Deserializer<T> { ... }
fun <T> Deserializer<T>.optional(): Deserializer<T?> {
    val parent = this
    return object : Deserializer<T?> {
        override fun deserialize(data: ByteArray?): T? {
            return data?.let {
                parent.deserialize(data)
            }
        }
    }
}
inline fun <reified T> deserializer() : Deserializer<T> {
   return (null is T) ? lookup(T::class.java).optional() : lookup(T::class.java)
}

If you think about it, T::class.java isn't type Class<T>. It's Class<T!>

Rather than being an assertion that T is not a nullable type, T! could 1) express that a type is the non-nullable version of a potentially nullable type, and 2) act as an operator on a reified type that outputs the non-nullable version. #2 would allow lookup(T!::class.java) above.

In this way, it would be an effective complement to T?.

@YoshiRulz
Copy link

There seems to be only one use-case for this, implementing a Java interface like the one in the proposal:

public interface JBox<T> { // I'm assuming putting the type parameter on the method was a typo
    void put(@NotNull T t);
}

This feature is only necessary for Kotlin/JVM, so it should be done with @Jvm* annotations like previous JVM-specific features—this was suggested earlier in #268 (comment). In pure Kotlin, such an interface can be made today without any new syntax:

public interface Box<T : Any> {
    fun put(t: T)
}
// Better example which uses both nullable and non-nullable in the signature. Imagine this function returns the previously held value, or null on the first call.
public interface Box<T : Any> {
    fun set(t: T): T?
}

@roxton Kotlin doesn't have a ternary operator ?:. Is this what you were going for?

fun interface Deserializer<T> {
    fun deserialize(data: ByteArray?): T?
}
fun <T : Any> lookup(type: KClass<T>): Deserializer<T> = TODO()
fun <T> Deserializer<T>.optional(): Deserializer<T?> = Deserializer({ it?.let(this@optional::deserialize) })
inline fun <reified T> deserializer(): Deserializer<T> = if (null is T) lookup(T::class).optional() else lookup(T::class)
// call-site:
val d = deserializer<User>() // d is Deserializer<User>
val d1 = deserializer<User?>() // d1 is Deserializer<User?>, which is a wrapper over a Deserializer<User>

(TIL null is T is valid for reified type parameters.)

If you think about it, T::class.java isn't type Class<T>. It's Class<T!>

Not sure where this came from. The type of T::class is not KClass<T!>, it's KClass<T>, and the java extension property preserves the nullability. In this case T : Any? from the function signature, meaning it won't compile because the signature for lookup can't be inferred, not even in the null !is T branch (there's no smart cast to T : Any).

Sure, maybe some form of intersection types will let this compile, or an annotation or whatever. But because the function is going to be inlined anyway, why not just do this:

inline fun <reified T : Any> deserializerFor(): Deserializer<T> = lookup(T::class)
inline fun <reified T : Any> deserializerForNullable(): Deserializer<T?> = lookup(T::class).optional()

Or only have the first function and add .optional() at the call-site.

@AndroidDeveloperLB
Copy link

Is the "& Any" becoming official?
Why choose this weird syntax? Is it from another language? How do you read it?
Can anyone please explain the logic behind choosing it? I've never seen such a thing and maybe by reading the logic of it, I will remember to use it.

@quickstep24
Copy link

Is the "& Any" becoming official? Why choose this weird syntax? Is it from another language? How do you read it? Can anyone please explain the logic behind choosing it? I've never seen such a thing and maybe by reading the logic of it, I will remember to use it.

The & indicates an intersection. Cloneable & Serializable are all types that are Cloneable and Serializable.
Any covers all non-nullable types (contrary to Any?, which includes nullable types), so if a type T includes null, then T & Any will be T without null.
Kotlin currently has limited support for intersection types, but wider support is in discussion.

@YoshiRulz
Copy link

YoshiRulz commented Mar 24, 2023

It's borrowed from Java, which probably borrowed it from an older language but that was before my time so IDK.

@AndroidDeveloperLB
Copy link

AndroidDeveloperLB commented Mar 24, 2023

@quickstep24 I see now the point in this.
Still seems weird.
Speaking about Any?, doesn't it mean that I can even use this useless thing : T & Any? ? . It means it's nullable, yet everything is already always nullable by default (like on Java), no?

@YoshiRulz Where do you see it in Java? In the code that caused it to appear (Glide in my case) I don't see in Java what has caused it...
Maybe you mean from a relatively new Java version (I use on Android, so Java version is very much behind, sadly)?

@YoshiRulz
Copy link

Bounds on type parameters can include intersection types, and it's certainly not a new feature. In Kotlin you'd use multiple where clauses.

@AndroidDeveloperLB
Copy link

AndroidDeveloperLB commented Mar 25, 2023

@YoshiRulz So how does a non-null type appears in Java for the generic type ("T") ?
Looking at the code of Glide, I didn't see there anything special that I don't already know of.
This is what I see there:

public abstract class CustomTarget<T> implements Target<T> {

Where "Target" is as such:

public interface Target<R> extends LifecycleListener {

   void onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition);

I don't see R or T marked as "always not null". It's just that currently, all functions (I've shown only what's relevant to the function I use) have them non-null.

@YoshiRulz
Copy link

I think there's been a slight misunderstanding; I never meant to imply that this feature as borrowed from Java, only that the & syntax was. My understanding is that Java's type system isn't concerned with null at all, hence the annotations.

@AndroidDeveloperLB
Copy link

@YoshiRulz Oh ok.
So how does the IDE decides that I should use it from the Java code? Not by actual declaration, but actually by checking the annotations on Java, seeing that all usages mean it's non-null?
Does it do it for Kotlin too (in case I extend a class that doesn't have the & Any yet all usages are non-null) ?

@YoshiRulz
Copy link

¯\_(ツ)_/¯

@AndroidDeveloperLB
Copy link

@YoshiRulz OK guys thank you for your patience and for your time.

@BreimerR
Copy link

image
In my assumption if case val res: String? is a warning shouldn't getOrDefault<String?> be a warning as well?

@cccccccmcho
Copy link

Is this feature enforced since kotlin 1.9 and later?
I'm getting an error in compile since upgrading from 1.8

@YoshiRulz
Copy link

https://kotlinlang.org/docs/whatsnew17.html#stable-definitely-non-nullable-types

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

No branches or pull requests