Skip to content

A New UnsafeNulls Language Feature for Explicit Nulls #9884

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

Merged
merged 2 commits into from
Mar 10, 2021

Conversation

noti0na1
Copy link
Member

This PR removes the UncheckedNull type, and adds a new language feature unsafeNulls to explicit nulls, which allows unsafe null operations in a certain scope. In addition, a relaxed overriding rule is implemented for overriding Java classes.

Description

In the original explicit nulls design, UncheckedNull was introduced to allow member selections on nullable objects from Java code (with type T | UncheckedNull). We could define UncheckedNull as a type alias of Null or a true subtype of Null; however, we find it is difficult to implement in both ways. See #7828 for more discussion.

We decided to create a new language feature, called unsafeNulls. Inside this "unsafe" scope, all T | Null objects can be used as T, and Null keeps being a subtype of Any.

Users can import scala.language.unsafeNulls to create such scope, or use -language:unsafeNulls to enable this feature globally. The following unsafe null operations will apply to all nullable types:

  1. select member of T on T | Null object
  2. call extension methods of T on T | Null
  3. convert T1 to T2 if T1.stripAllNulls <:< T2.stripAllNulls or T1 is Null and T2 has null value after erasure
  4. allow equality check between T and T | Null

The intention of this unsafeNulls is to give users a better migration path for explicit nulls. The UncheckedNull is only limited to unsafe member selections. We think this approach can do more than UncheckedNull and in a better way.

Projects for Scala2 or regular dotty can try this by adding -Yexplicit-nulls -language:unsafeNulls to the compile options. A small number of manual modifications are expected (for example, some code relies on the facts of Null <:< AnyRef). To migrate to full explicit nulls in the future, -language:unsafeNulls can be dropped and add import scala.language.unsafeNulls only when needed.

Examples

All the examples are under -Yexplicit-nulls flag.

def f(x: String): String = ???

import scala.language.unsafeNulls

val s: String | Null = ???
val a: String = s // unsafely convert String | Null to String

val b1 = s.trim() // call .trim() on String | Null unsafely
val b2 = b1.length()

f(s).trim() // pass String | Null as an argument of type String unsafely

val c: String = null // Null to String

val d1: Array[String] = ???
val d2: Array[String | Null] = d1 // unsafely convert Array[String] to Array[String | Null]
val d3: Array[String] = Array(null) // unsafe

Without the unsafeNulls, all these unsafe operations will not be compiled.

unsafeNulls also works for extension methods and implicit search.

import scala.language.unsafeNulls

val x = "hello, world!".split(" ").map(_.length)

given Conversion[String, Array[String]] = _ => ???

val y: String | Null = ???
val z: Array[String | Null] = y 

See more examples about unsafeNulls in tests/explicit-nulls/pos/unsafe-*.scala.

An example for relaxed overriding:

abstract class J {
  abstract void f(String x);
}
// both S1 and S2 are acceptable

class S1 extends J {
  override def f(x: String) = ???
}

class S2 extends J {
  override def f(x: String | Null) = ???
}

See more examples about overriding in tests/explicit-nulls/pos/override-*.

We are also considering a Java-compatible flag, which enables unsafeNulls to all Java method calls. This will be in a seperate PR.

@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from 8a8d308 to 3923aa5 Compare September 26, 2020 21:16
@noti0na1 noti0na1 requested a review from odersky September 26, 2020 22:46
@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 4 times, most recently from c5909d2 to da75df4 Compare October 5, 2020 15:08
@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from b68ed5c to 04982b1 Compare October 7, 2020 23:33
@odersky
Copy link
Contributor

odersky commented Oct 8, 2020

test performance please

@dottybot
Copy link
Member

dottybot commented Oct 8, 2020

performance test scheduled: 1 job(s) in queue, 1 running.

@dottybot
Copy link
Member

dottybot commented Oct 8, 2020

performance test failed:

Please check http://lamppc37.epfl.ch:8000/pull-9884-10-08-14.45.out for more information

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a performance test for this. I tried but the scodec conflicts need to be resolved first.

Overall, I think the approach is workable, but we should work on streamlining with the goals of increasing clarity and efficiency.

My main concern is that there are currently too many booleans that are tested in too many places. We should try to standardize on a minimal set of switches only and define them all in the same place (probably as Mode bits). Then, add a doc comment explaining what each boolean means and where it is used, and why it is different from the others.

@noti0na1
Copy link
Member Author

noti0na1 commented Oct 8, 2020

@odersky I have rebased the branch and scodec. Will the performance test run if I comment "test performance please" here?

@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from 84e8d0b to b4376a1 Compare November 12, 2020 05:51
@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from 3c67572 to afa320c Compare November 25, 2020 03:49
@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from 008b21a to fd83a44 Compare December 8, 2020 22:47
@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from ac266a0 to 08fc382 Compare December 23, 2020 02:36
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass. I have not looked at everything yet. The general direction looks good IMO but right now I find the logic complicated to follow because there are too many mode switches represented as boolean parameters with defaults. So before going further, I have the following suggestion:

Try to formulate the logic without using any boolean default parameters. Work on streamlining things in the context instead. Default parameters are a too easy way to hide complicated logic. Once that is done, consider bringing back some selected default parameters, but only if that leads to performance improvements or more streamlined code (but only after all other alternatives to streamline code have been considered).

@odersky
Copy link
Contributor

odersky commented Jan 2, 2021

I also added a commit with some small fixes of typos and wordings.

@odersky
Copy link
Contributor

odersky commented Jan 12, 2021

Hi @noti0na1. What's the current state of this? Do you still want to work on improvements here?

I just had a look again, but I still find there are too many modes and default parameters floating around. Is there no way to bundle all of it in a mode bit in the context, say?

Please re-assign to me, when you think this is ready for review again

@noti0na1
Copy link
Member Author

noti0na1 commented Feb 7, 2021

@odersky I have combined all the commits.

@odersky
Copy link
Contributor

odersky commented Feb 10, 2021

test performance please

@dottybot
Copy link
Member

performance test scheduled: 2 job(s) in queue, 1 running.

@dottybot
Copy link
Member

performance test failed:

Please check http://lamppc37.epfl.ch:8000/pull-9884-02-10-18.40.out for more information

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR still has a lot of complications, which might not be necessary. To be able to merge this, it should be simplified ruthlessly. I mean not just a few cosmetic changes but every element of this PR should be put to the test whether it is really needed. I did that with #11375, and found that a lot of concepts seem not to be needed, after all. At least all the explicitNulls tests pass if they are removed. Among the removed constructs are:

  • The UnsafeNullsSubType mode
  • The double caches in Implicits
  • The withOrNull in translateParameterized
  • The special case in adapt
  • A few other smaller things

So, maybe it's best to take #11375 as a basis, and only add some specific changes to this simpler base that are needed for correctness.

The reason I am so strict here is that I worry about future maintainability of the compiler codebase. Maintainers need to be able to verify and modify every single element of the codebase. Unnecessary code, and worse, unnecessary abstractions and conditions are
very bad for this since they destroy confidence that the code we deal with is there for a reason.

@odersky
Copy link
Contributor

odersky commented Feb 11, 2021

What about we try the following procedure to push this over the finish line?

  • Start with Attempt to Simplify UnsafeNulls #11375 as the commonly agreed basis. (If possible, split it again in several commits that handle individual features, but this is not mandatory),
  • Look at each remaining failure in the community build and minimize it into a test. Then, add the simplest fix with a comment in the code that refers to the failing test. That way, we can have an audit trail that tells us why the new distinctions and changes are required.

When in doubt, let's discuss each failure and proposed fix together.

@odersky odersky assigned noti0na1 and unassigned odersky Feb 11, 2021
@odersky
Copy link
Contributor

odersky commented Feb 15, 2021

Turns out the only failing CB test in #11375 is shapeless. This could be since shapeless has not been updated like it has here. @noti0na1 can you do the update a see what the status is?

@noti0na1
Copy link
Member Author

@odersky Sorry for the late response. I will merge and look at these changes soon.

@noti0na1 noti0na1 force-pushed the UnsafeNulls branch 2 times, most recently from 6cc2028 to f6e6385 Compare February 16, 2021 00:09
@noti0na1
Copy link
Member Author

@odersky I have merged the changes from your PR. I think the introducing of SafeNulls really simplifies the complexity.

These are several tests for eligibleCache (searching inside and outside of unsafeNulls scope at the same time), and they all pass the test.

I will update the document recently.

@odersky
Copy link
Contributor

odersky commented Mar 9, 2021

@noti0na1 Should we merge this then?

@odersky
Copy link
Contributor

odersky commented Mar 10, 2021

Looks very good now! I think this is a great basis to experiment with and to possibly refine things later.

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.

4 participants