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

Avoid giving the result of loads a non-null type. #1805

Merged
merged 6 commits into from
Dec 5, 2024

Conversation

cpovirk
Copy link
Collaborator

@cpovirk cpovirk commented Dec 3, 2024

2542ca8
changed types like the return type of Cache.get to be non-null instead
of unspecified.

As discussed in #594, the
types really "should" be nullable, but that is inconvenient for most
users.

When Caffeine was using Checker Framework annotations, it could use
@PolyNull to give Checker Framework users the best of both worlds and
give users of other tools (notably Kotlin) a convenient (if not fully
strict) experience, since those tools viewed the types as having
unspecified nullness (in Kotlin terms, "platform types").

Caffeine could still use @PolyNull today if you want to restore the
Checker Framework dependency. (And maybe someday JSpecify will include
its own version, as discussed in
jspecify/jspecify#79.)

But the important thing is that Caffeine not affirmatively make the
type be non-null. And that's what @NullMarked does. That would cause
trouble for a number of Kotlin users in Google's codebase.

So, to undo that, we can use @NullUnmarked (recognized by Kotlin as of
2.0.20) on the methods where we want to keep types unspecified. Then we
can optionally use @NonNull annotations on the other types in the
API in order to still make those types non-null.

Kotlin actually still doesn't quite get the types right, thanks to
https://youtrack.jetbrains.com/issue/KT-73658. But this PR at least
annotates correctly (I hope :)), which may help other tools even today
and which should help Kotlin eventually.

If enough people run into the Kotlin bug in practice, then we could
consider backing out even more nullness information: If we were to
remove @NullMarked from Cache+AsyncCache and then potentially
sprinkle in some @NonNull annotations to compensate, then we could
still preserve at least some of the new information in those classes.
And of course other classes can remain @NullMarked.

ben-manes@2542ca8
changed types like the return type of `Cache.get` to be non-null instead
of unspecified.

As discussed in ben-manes#594, the
types really "should" be nullable, but that is inconvenient for most
users.

When Caffeine was using Checker Framework annotations, it could use
`@PolyNull` to give Checker Framework users the best of both worlds and
give users of other tools (notably Kotlin) a convenient (if not fully
strict) experience, since those tools viewed the types as having
unspecified nullness (in Kotlin terms, "platform types").

Caffeine could still use `@PolyNull` today if you want to restore the
Checker Framework dependency. (And maybe someday JSpecify will include
its own version, as discussed in
jspecify/jspecify#79.)

But the important thing is that Caffeine _not_ affirmatively make the
type be non-null. And that's what `@NullMarked` does. That would cause
trouble for a number of Kotlin users in Google's codebase.

So, to undo that, we can use `@NullUnmarked` (recognized by Kotlin as of
2.0.20) on the methods where we want to keep types unspecified. Then we
can optionally use `@NonNull` annotations on the _other_ types in the
API in order to still make those types non-null.

Kotlin actually _still_ doesn't quite get the types right, thanks to
https://youtrack.jetbrains.com/issue/KT-73658. But this PR at least
annotates correctly (I hope :)), which may help other tools even today
and which should help Kotlin eventually.

If enough people run into the Kotlin bug in practice, then we could
consider backing out even more nullness information: If we were to
remove `@NullMarked` from `Cache`+`AsyncCache` and then potentially
sprinkle in some `@NonNull` annotations to compensate, then we could
still preserve at least some of the new information in those classes.
And of course other classes can remain `@NullMarked`.
@CLAassistant
Copy link

CLAassistant commented Dec 3, 2024

CLA assistant check
All committers have signed the CLA.

@ben-manes
Copy link
Owner

Feel welcome to merge when you are comfortable with the changes.

@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 3, 2024

Thanks for the review, and thanks for giving the JSpecify annotations a spin in the first place!

It looks like I may end up wanting to touch AsyncCacheLoader and CacheLoader in similar ways. Let me know if you have any preference between adding those changes to this PR (and giving you another chance to review) and submitting a separate PR.

@ben-manes
Copy link
Owner

I have no preference, and appreciate the thoughtfulness in the contribution.

@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 3, 2024

Thanks. I will be waiting to hear back from our open-source-licensing people in any case. Hopefully the process will be relatively quick, since (I think you've said) your CLA is basically our CLA :)

@ben-manes
Copy link
Owner

It is just the Apache Foundation's, which merely reiterates the license as a notice for an optional explicit agreement instead of an implicit one through the act of contributing. Its entirely optional if not signing is preferable.

The CCLA is a backup document that the committer/ICLA signer may use to eliminate all of the ambiguity between conflicting laws, contracts, policies and job assignments. We've never required it. Many committers are confident of their individual representations under the ICLA, many other committers find it reassuring that their company has backed up their own ICLA with this umbrella document.

https://www.apache.org/licenses/cla-faq.html#cclas-not-required

This makes them match what `CacheLoader` already allows for synchronous
loading and reloading via `@Nullable V load(K key)` and `@Nullable V
reload(K key, V oldValue)`.
@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 3, 2024

Thanks. If the licensing people take a while to get back to me, then I can merge without signing.

I've added the changes to the loader classes. I'll probably make sure that I have all our depot's tests passing before I merge anything here.

(I could believe that there are other types in Caffeine that could still benefit from annotations, especially more of those found in type arguments. I'm not currently planning to look proactively, but we'll see if any more come to my attention or if I go looking for a distraction :))

@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 4, 2024

I found a few more, not through build/test failures but just through further examination.

And I was doing the further examination because I am trying to get together an alternative approach: We could allow the various cache classes to have a nullable value type. That wouldn't mean that the caches could "contain" null, since they never cache that value. What it would mean is that the cache uses/supports a loader that can return null, which in turn means that the computing get methods can also return null. That would allow us to give a specified return type to all the methods, with users able to declare either "a cache that might return null" or "a cache that will never return null." I have an attempt at that in a Piper CL that I'm testing against the depot. I'll see how it looks, and then I'll put together a separate PR so that you can see what you think.

In the meantime, I'm testing the latest version of this PR :)

@ben-manes
Copy link
Owner

oh that sounds pretty amazing if it can work that way!

If helpful, the examples section acts as integration tests using separate builds. This way I could avoid complexity in the main build, like Graal AOT set up, by isolating those tooling scenarios. Then I write it up under the guise of a tutorial, somewhat to help me remember how it works, and I have regression test that can run against the locally built jar in CI. That approach might be attractive to you if nullability is currently best verified in Kotlin so that I don't mangle that in the future.

@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 5, 2024

Great!

My new attempt at annotating seems to be working well: It requires fewer updates to users (not that I needed to make too many changes for your original change plus this PR, but less is nice), and it ensures that anyone who passes a null-returning loader will have to handle null during cache operations. It's passing all our tests.

Meanwhile, I did get this PR passing all our tests, as well. My plan is to merge this one (to at least immediately eliminate the non-null types that were my main worry) and then create another PR that shows my new attempt as a diff against this one.

(I even got approval for the CLA :))

@cpovirk cpovirk merged commit 047021c into ben-manes:master Dec 5, 2024
142 checks passed
@cpovirk cpovirk deleted the unmark branch December 5, 2024 14:20
@cpovirk
Copy link
Collaborator Author

cpovirk commented Dec 5, 2024

Sorry, I just noticed that the default is "Merge pull request" but that the Caffeine history looks linear, at least recently. It might have been better for me to either squash or rebase.

cpovirk added a commit to cpovirk/caffeine that referenced this pull request Dec 5, 2024
This does not mean that the cache will "contain" `null`, since `null` is
never cached. However, it does allow users to declare whether they want
"a cache whose loads might return `null`" (which thus can return `null`
from cache operations) or "a cache whose loads will never return `null`"
(which thus doesn't require users to handle `null` when they request
that the cache return or load a value).

This PR follows up on ben-manes#1805.

Ideally I will come back to write some Kotlin tests for this. For the
moment, I can only say that the results look good in my testing inside
Google.
@ben-manes
Copy link
Owner

No problem. I disabled the other options going forward so squash is required. There is no default, only the last option selected by the user.

cpovirk added a commit that referenced this pull request Dec 10, 2024
…1806)

This does not mean that the cache will "contain" `null`, since `null` is
never cached. However, it does allow users to declare whether they want
"a cache whose loads might return `null`" (which thus can return `null`
from cache operations) or "a cache whose loads will never return `null`"
(which thus doesn't require users to handle `null` when they request
that the cache return or load a value).

This PR follows up on #1805.
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 this pull request may close these issues.

3 participants