-
Notifications
You must be signed in to change notification settings - Fork 207
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
Do we need a non-nullable type operator (T!
)?
#142
Comments
My take, for the record, is that I don't know that we need it. I think the simplest option is to assume we don't and then see how migrating the core libraries go without it. |
Example from @lrhn where it would be good to have: places where you want to use null checks to promote to a non-nullable type. // Note, non-nullable bound on type parameter
class Box<T extends Object> {
T x;
Box(this.x);
}
class F<T extends Object?> {
Box<T!> box(T x) {
if (x != null) return Box<T!>(x);
throw "Something";
}
} Without the |
To explore this question further, I worked out a draft of the equational theory here. It seems to work out, but is a bit heavyweight. For the most part, I think implementations could maintain a normal form that avoids having to deal with nesting of |
A couple of comments on other languages. Kotlin doesn't seem to have an explicit version of this that I can find. However, it does seem to be able to represent non-nullable versions of type variables, either in the type system, or via some sort of parallel tracking. Example: fun <S : Int?, T : S>testNullAssertion(x : T) {
x.div(3); // Errror
if (x != null) {
x.div(3); // Ok
}
var a = x!!;
a.div(3); // Ok
} In Typescript, you can represent this directly using intersection types: function check(s: String | null) {
s.length; // Error
checkNull(s).length; // No error
}
function checkNull<T>(x: T): T & Object {
if (x == null) {
throw "Null error";
} else {
return x;
}
} This is relevant to #196, since without this type operator we can't express the non-nullability of the result of a null assertion operator in the type system, and would have to do something separate (as Kotlin appears to do). |
Ah, dang, you're right. Otherwise: foo<S extends Int?, T extends S>(T value) {
var checked = value!; // What is the type of checked?
checked + 1; // How is this allowed?
} We could conceivably restrict a bare That feels pretty hokey to me, though. What about: foo<S extends Int?, T extends S>(T value) {
if (value != null) {
value + 1; // Allowed?
}
} Intuitively, I would expect this to promote and thus allow the |
Just a thought. If this is only for rare cases perhaps a function (like |
If we want to track it is part of the type system, yes. It's conceivable that we could specify the errors and warnings for method calls via a notion of nullability that incorporates (but supersedes) the type. More or less, I think this is mostly equivalent to having |
I'm not sure I see the issue. Is there something particular about |
You do need to account for how
where
I think that works out. |
My main concern with not having this feature is that it makes it hard to explain what the type of For simple types, it's clear what we can do: foo(int? value) {
value!.isEven; // value! has type int
var x = value!;
var l = [x]; // inferred as List<int>
if (value != null) { // value is promoted to int
value + 1;
}
} We can extend this to type variables with concrete bounds by promoting the bound: foo<T extends int?>(T value) {
value!.isEven; // value! has type `T extends int` (or `T & int`)
var x = value!;
var l = [x]; // inferred as List<T>
if (value != null) { // value is promoted to `T extends int` (or `T & int`)
value + 1;
}
} This does have the same unfortunate issue that we have with existing promotion on type variables: namely that inference needs to strip off the promoted type, at least in reified positions. So note the inferred type of The trickier case is type variables with chained bounds. Consider the following example: foo<S extends Int?, T extends S>(T value) {
value!.isEven;
var x = value!;
var l = [x];
if (value != null) {
value + 1;
}
} One answer is to simply say that this code is invalid: that is, we don't promote on these kind of type variables, and The latter is really unfortunate, so a second answer is to instead say that A final possible answer is to allow promotion based on the ultimate bound after chasing through the chain of type variables. That is, in the intersection type terminology, treat This has the same unfortunate behavior around inference and reification. We would not be able to infer any type for Overall though, this feels like a solution that covers a lot of ground, and the inference anomalies are at least consistent with how we deal with promotion. |
I think being consistent with promotion is important (and probably a requirement of a consistent system). We should be able to define The edge cases are obviously the tricky ones.
The ground rules I imagine are:
Since An alternative is If
That leaves type variables. If Otherwise, if the type
That is exactly what we would get if we actually promoted (We probably also want to allow promotions to non-nullable thorough num? x = ...;
if (x is Object) {
// x is num? & Object = (num & Object)(?&!) = num
...
}
num y = ...l
if (y is int?) {
// y is num & int? = (num & int)(!&?) = int
} That is: treat nullability and type as different and independent axes, so even if |
I don't claim to understand all of this, but at a high level it sounds reasonable to me.
I think this roughly parallels this code: Future<num> y = ...
if (y is FutureOr<int>) {
// ...
} In principle, you could imagine promoting to Given that, can we just not support promoting around nullability in similar cases too? |
This doesn't work. A type variable |
Well, this is an odd fit with the interpretation of It's also weird that: T! foo<T>() => (null as T!)
var x = foo<dynamic>(); is valid dart code that succeeds, and binds
This is an axiom in the intersection type interpretation I proposed here |
Type variables ... are indeed annoying because The way I think of type equality is in terms of a normal form reduction. I should try to complete writing that down.
etc.
and then I want some consistent view of which types are nullable and which are non-nullable, and type variables (and only type variables, potentially promoted) can actually be neither. About the T! foo<T>() => (null as T!)
var x = foo<dynamic>(); succeeds because there is no T! foo<T>() => (null as T!)
Object? x = foo<Object?>(); If we replace So, as usual, we will need to distinguish the static type system and the run-time type system, where The static inference of the first example above succeeds because I do think that we should be careful about promoting foo(dynamic something) {
if (something != null) { something.floo(); }
} Here the check We do allow promoting I'm more worried about |
no. |
This makes no sense to me whatsoever. You are proposing that
There is a clean, simple, interpretation, which works out entirely consistently. That interpretation is that
and the same for |
That's (perhaps) fine, but you need to understand that you are performing reduction on open terms. For closed terms, you can generally get all of the equations that you want by reducing to a normal form, and comparing. But that is not necessarily the case for open terms. Some systems do have the property that you can define the equational theory in terms of "normalize and compare", but others just flat out don't. I really don't think that you can use normalization to avoid reasoning about the equational theory of |
It's not desired. In fact, it's... completely wrong. :)
This is an admissible axiom given my proposed rules (if by equality you mean mutual subtype), but it doesn't give you associativity. Why do you think it does? |
@tatumizer You're right - I misread what you were saying. This is getting a bit noisy, can we drop the topic, or take it offline, or to a separate topic specific issue? To summarize the properties that we do want (and I believe do get with my proposed rules):
I believe that all of these properties are satisfied by the rules I have proposed. See in particular the application of the distributivity of intersection over union in the development of the algorithmic rules. If you believe that I've made a mistake there (this is very possible!) and some of these equations are not derivable, can you file a separate issue with a concrete example of the missing derivation? For the purposes of this issue, let's take it as a given that we can make the type theory work so that we can focus on the question of the utility of the operator. |
If we look at the type promotion, we already introduce intersection types in some cases. I expect
A problem with having an explicit foo<T extends Widget>(T x) {
if (x is SubWidget) {
var w = x; // static type of w is *not* T&SubWIdget, just T, but perhaps known to be T&SubWidget
}
} Doing the same for non-null: foo<T extends Object?>(T x) {
if (x is Object) {
var w = x; // static type of w is `T`, but not `T & Object`
}
} would still allow us to avoid reifying the intersection type. foo<T extends Object?>(T x) {
T! w = x!
} then either that doesn't do what they expect, or it reifies the intersection type. Both are bad. Not doing a perfect implicit promotion is easier to defend than not doing what was explicitly asked for. |
@leafpetersen, @lrhn, @munificent, are we gravitating in the direction of "no, we won't have this feature"? |
Not planned. |
Are there any plans to add something like this? I ran into wanting this today to add a simple extension function to Stream. What I wanted to do was (something like) this: extension NullableStreamHelpers<T extends Object?> on Stream<T> {
Stream<T!> ignoreNulls() => where((event) => event != null).map((event) => event!);
} As far as I know, this isn't possible in dart? I've seen this kind of thing used to great effect in Swift. Apologies is this has been answered elsewhere but it's been more than a year and I'd figure I'd bump this again. 😊 PS: I'm not sure the |
@ajmalk You can use |
You can't, becuase he wants to allow the extension to be used on streams of nullable types, to ignore those nulls. He wants a way to say "allow this on a stream of nullables, but this method only returns the non-nullable version of that type". I think that's pretty reasonable. |
What you want is probably: extension NullableStreamHelpers<T> on Stream<T?> {
Stream<T> ignoreNulls() => whereType<T>();
} |
Well extension NullableStreamHelpers<T> on Stream<T?> {
/// Making [R] a subtype of [T?], avoids cases where there can't be a match, like `int?` -> `String`
Stream<R> whereType<R extends T?>() => where((event) => event is R).cast<R>();
Stream<T> ignoreNulls() => whereType<T>();
} |
We have whereNotNull for iterables in |
This doesn't work for extension types :(, they aren't objects |
@dickermoshe extension type Id(int _id) implements Object {
// Makes the extension type non-nullable.
static Id? tryParse(String source) => int.tryParse(source) as Id?;
} |
[EDIT] The resolution of this is that we're not taking this approach now. We could reconsider in the future if we have demand for it. A workup of the equational theory was done here.
This issue is for discussion of the question of whether we should add a type operator which strips off the nullability from a type. That is, if
T
is a type, thenT!
would be the non-nullable version ofT
.cc @lrhn @munificent @eernstg
The text was updated successfully, but these errors were encountered: