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

Should Object be nullable by default? #141

Closed
Tracked by #110
leafpetersen opened this issue Dec 17, 2018 · 44 comments
Closed
Tracked by #110

Should Object be nullable by default? #141

leafpetersen opened this issue Dec 17, 2018 · 44 comments
Assignees
Labels
nnbd NNBD related issues

Comments

@leafpetersen
Copy link
Member

leafpetersen commented Dec 17, 2018

This issue is to discuss the question of whether the Object type should be nullable by default when we enable NNBD types.

Arguments I've hard so far:

For:

  • Since null supports all of the operations that are defined on Object, it works perfectly well anywhere an Object is expected.
  • It's not clear what tangible benefit you get from being able to say that something can be any type, except Null.

Against:

  • There is no way to express the type of a non-null object without this. So you can't write a generic class which requires only non-nullable type arguments. Does this matter?
  • You won't get definite assignment warnings on variables of type Object.

cc @lrhn @munificent @eernstg

@kevmoo
Copy link
Member

kevmoo commented Dec 17, 2018

From an end-user perspective, having Object be "different" here would be a bit confusing.

As an experiment, JsonDecoder now is Converter<String, Object>. null is a totally valid output.

In a world without nullable Object, what would the 2nd type argument be?

@leafpetersen
Copy link
Member Author

In a world without nullable Object, what would the 2nd type argument be?

Object?

@leafpetersen leafpetersen changed the title Should Object be nullable? Should Object be nullable by default? Dec 17, 2018
@kevmoo
Copy link
Member

kevmoo commented Dec 18, 2018

Oh! So, Object would just be like Customer – that's what I hoped.

So put me in the against camp. 😄

@leafpetersen
Copy link
Member Author

A minor concern that I have with making Object nullable is that if we have neither non-nullable Object nor T!, then there is no user visible type that we can ascribe to x in the following code if we promote:

foo(Object nullable) {
  if (nullable != null) {
    var x = nullable;  
  }
}

On the other hand, there is essentially no difference between Object? and Object! in terms of methods available on them, so it's not all that useful to know that x is non-nullable.

@eernstg
Copy link
Member

eernstg commented Dec 20, 2018

My general preference is to let Object denote all objects other than the null object (that is, to make it non-null).

There is no way to express the type of a non-null object without this

Right, to me that's the main reason. But there's a technical as well as a conceptual side to it.

Technically, we'd otherwise need a special exception to even denote the set 'all objects other than the null object'. As mentioned, we'd use Object!, but that would already be a special exception (because ! is otherwise never used, or at least needed, on a statically resolved type).

Technically, we are also well positioned to claim that Object does not include the null object: Just like all other types today, they are implicitly subject to the transformation T --> T | Null almost everywhere. So the only breaking change would be that null is Object will now be false.

But, even more importantly, I think it is useful to support the concept of having an object "which is actually there", as opposed to the null object which is hardly ever used because it is useful in its own right, it is always used in order to indicate that "something else could be here, but it was omitted or it is not yet in place".

In contrast, I think it makes sense to be maximally permissive with dynamic and void, so they should be nullable (and I don't care if developers get less-than-optimal support for expressing dynamic!, not to mention void! ;-).

We might want to say that dynamic and Object then evaluate to different instances of Type, because they do not include the same set of instances any more, and that might be breaking. Not sure how dangerous that is, though...

@lrhn
Copy link
Member

lrhn commented Dec 20, 2018

One user input is that it would be surprising if List<Object> (no "?" in there) could contain null.

For consistency, it would be nice if Object was non-nullable and Object? was nullable, just as for any other type (well, except Null which is the same type as Null?).

If we go that way, then we need to define what Null is if it's not a sub-class of Object.
We could just say that it's another class with no supertype, like Object and it has the same methods, and you can use those methods on nullable types.

Or, as Erik suggested, we could say that there exists a class which is represented by Object? and which has Object and Null as subclasses, and no superclass itself. It's the new top-object-type, and we just don't tell anyone its real name, so they have to refer to it as Object?, and it's what you get when you nullify Object. That requires some special-casing in the specification, but then some other things fall out automatically. I don't think it's distinguishable from the previous two-top-classes design from a user perspective, it's all about how to specify the behavior, and which things we get for free.

@eernstg
Copy link
Member

eernstg commented Dec 20, 2018

I think we should have a class and a reified representation of that class corresponding to the top type, such that we have a simple story about why it is possible to say o.toString() etc. when o is "anything", including a function object and null. Evaluation of dynamic and void should also return a reification of the top type (although they are allowed to return 'dynamic' and 'void' from toString()).

It may look like an exception, but I think it's OK for Object? (if we allow that to be an expression, syntactically, otherwise it could certainly be the value of a type variable) to evaluate to a Type instance which represents the top type, rather than evaluating to the Type for Object in some wrapper that means | Null. We could also allow such non-normalized top type reifications to exist, and just insist that they compare equal to the plain top type according to ==, but I suspect that we would offer a better service to developers if we normalize eagerly.

With this firm notion of a top type, we can maintain a simple conceptual model about what it means to be an "object", and we also have the simple model that being an Object means being an "object" which is not the null object.

@leafpetersen
Copy link
Member Author

Technically, we'd otherwise need a special exception to even denote the set 'all objects other than the null object'.

I think part of the question here is, why would we need this? I haven't really seen an argument for it yet.

@eernstg
Copy link
Member

eernstg commented Dec 20, 2018

I think the notion of 'all objects other than the null object' is conceptually useful, because the null object is almost always used to indicate that something is not available (or not yet available), so we may wish to indicate that we won't encounter any "missing objects".

For instance, a Set like data structure could use this type as the bound on its type variable, indicating that it makes no sense for null to be a member of it.

If you buy the idea that it is useful, it seems more straightforward to me if Object denotes the non-null type and Object? is used to express the corresponding nullable type, rather than inverting it such that we must remember to use Object! respectively Object, just for that single type Object.

(And the special exception is that we wouldn't otherwise ever use ! on a compile-time resolved denotation of a type, and maybe we shouldn't even allow using it on anything other than a type variable, given that we could make it obviously and consistently useless on everything other than a type variable.)

@munificent
Copy link
Member

I think the notion of 'all objects other than the null object' is conceptually useful,

But is it practically useful? I'm fairly confident a more practically useful type would be String+, which is the set of strings that are not empty (""). intz, the set of ints except for zero would be nice for preventing divide-by-zero errors. We don't have any plans to add either of those, so it feels weird to me to put a lot of effort into defining a new type for "Object minus this one special value" when we don't have any know use cases for it.

For instance, a Set like data structure could use this type as the bound on its type variable, indicating that it makes no sense for null to be a member of it.

But we wouldn't do that for Set because it is useful for users to define sets that may contain null. And if that's useful for Set, I don't see why that wouldn't be useful for other user-defined set-like classes.

One way to resolve this would be to go through the core libraries and see if we can find any places where we'd use Object-null. If we can't find any, it's probably not a well-motivated feature. If we can, then great, let's do it. :)

@lrhn
Copy link
Member

lrhn commented Jan 3, 2019

In Dart, null is an object, because it's a value and all values are objects. If it is not an Object, then we have some notion of "object" that is different from is Object (and which would then be is Object?).

The only place where you would need Object instead of Object? is where null means something else than any other value. That is, where null means something. Say, Map<K,V>.operator[] which has return type V? and returns null on a non-entry and the entry value for an entry. If V is Object, then you can detect absence of an entry from that lookup. It might even make sense to force map values to always be non-nullable, so that assigning null as value for a key would remove the entry. This is what Expando does, and I think it's what we should have done from the start for maps and sets too (although I'd probably allow null as a map key, which feels odd if it can't be a value). It would be a breaking change to do it now, though.
(And it would break people who use maps as "sets of pairs" rather than actual maps).

I doubt there is any SDK code that we will actually change to use Object to not include null because it would be breaking, not because we don't want to or it wouldn't be useful. It's places where null already means absence, we could then prevent it from meaning something else.

I don't think making Object nullable will be an actual problem, as long as we have some other way to force a type argument to be non-nullable where possible, instead of just extends Object. There is no code that can accept Object instances that can't also handle null, because to all existing code null is an Object.

@leafpetersen
Copy link
Member Author

This may be a little tied up with the discussion of optional parameters and default values. I think there's been at least one proposal to have foo({int x}) mean that x is a required parameter (since it is non-nullable). But I'm not entirely sure how that is intended to play out with generics. If we take this interpretation, and we want to be able to say that foo<T>({T x}) has a required named parameter x, then we may need to be able to say that T is a non-nullable type parameter, which requires some non-nullable top type.

@eernstg
Copy link
Member

eernstg commented Jan 4, 2019

I wrote:

the notion of 'all objects other than the null object' is conceptually useful,

@munificent wrote:

But is it practically useful?

I do think so, yes. It is used by developers to write code with fewer bugs, because they can rely on a specific property to be maintained consistently.

The relevant property here is "the null object indicates that some object could be provided here, but it's absent". In other words, the null object is never provided for its own sake. This is also a built-in implication of the association of nullable types with optional types: If a nullable type T? is considered to be a short hand for the optional type Option<T> (the fact that we can have Option<Option<T>> etc, makes them different, but that's an orthogonal property) then the null object must be taken to mean that the object which is provided optionally wasn't there.

This interpretation is used by operator [] of a map. I consider it to be an error-prone property of that class that we can actually map a key to the null object, and the client can't see that this was the value for that key, rather than an indication that that key isn't present. It's possible that we can't fix this, because it could be a massively breaking change, but I still think that it would be a nicer and cleaner design if a null from operator [] would consistently mean "this key is not present". Even if we can't fix Map, we could allow developers to use such a clean design in newly written classes, and they could fix all the existing classes where it isn't so massively breaking.

@leafpetersen just mentioned another situation where the ability to express the type of "all objects except null" is useful (or even essential), namely as a type variable bound, in connection with the type of an optional parameter.

This makes me think that we do need to allow developers to express the type of all objects except null; and in that case I find it more readable and consistent to let Object be non-null, and to use Object? for the corresponding nullable type.

@lrhn
Copy link
Member

lrhn commented Jan 4, 2019

For foo<T>({T x}) ..., whether the argument is required would depend on the type argument.
If you write foo<int>(), it will have to be an error, and if you write foo<int?>() it is not.
If you want to ensure the argument is provided and not null (probably unless T is Object or Null), then write it as foo<T!>({T x}) ... or foo<T>({T! x})... (which potentially means that the type parameter itself is forced to non-nullable or the parameter type is mad non-nullable) to make the parameter type non-nullable even without a bound.

The function itself should not care whether its argument is required, if the body starts running, then the invocation was successful, it doesn't matter why it succeeded.

@eernstg
Copy link
Member

eernstg commented Jan 4, 2019

For foo({T x}) ..., whether the argument is required would depend on the type argument.

Which is the reason why I proposed that we could outlaw such a declaration entirely: It is an error for the type annotation on a named parameter with no default to have unknown nullability. You would then have to use T extends Object where T is declared (meaning: T cannot be a nullable type, assuming that we will allow Object to denote "all objects except null"), or use T! or T? where it is used, in order to denote a type with known nullability.

So we don't actually have to handle unknown nullability, because we don't have to introduce it.

ensure the argument is provided and not null

That would be ensured with T extends Object to declare T, or with {T! x} to declare x.

The function itself should not care whether its argument is required

I think it should be part of the type of the function that it accepts a named argument which is required, such that we can maintain the requirement also for first class functions. I'm not sure whether this is the same thing as "the function itself cares". ;-)

@leafpetersen
Copy link
Member Author

For foo<T>({T x}) ..., whether the argument is required would depend on the type argument.
I

So then is foo<T>({T x}) { foo<T>(); } disallowed?

@eernstg
Copy link
Member

eernstg commented Jan 4, 2019

If it is disallowed to even declare a named parameter like x with no default and unknown nullability then we probably wouldn't want to specify special exceptions where the body is such that it's OK anyway.

@munificent
Copy link
Member

munificent commented Jan 4, 2019

This interpretation is used by operator [] of a map. I consider it to be an error-prone property of that class that we can actually map a key to the null object, and the client can't see that this was the value for that key, rather than an indication that that key isn't present. It's possible that we can't fix this, because it could be a massively breaking change, but I still think that it would be a nicer and cleaner design if a null from operator [] would consistently mean "this key is not present".

I think the fundamental conceit of "nullable types" versus a real, nestable Option type is that, yes, the former is less expressive, but its convenience (and performance?) in common cases makes up for the lack of expressiveness.

You basically only get one sentinel value. That's enough most of the time, and then the simplicity of only having one means you can represent a potentially-absent reference using a NULL pointer. It means users don't have to worry about explicitly flattening unwrapping multiple layers when merging and composing collections containing potentially absent stuff. But it leads to ambiguity in some cases like operator [] on Map.

It's not a perfect solution, but it's the one we have. I don't think we'll do our users a better service by committing to applying that solution consistently then we would by giving them an ecosystem that uses null for "absent" in some APIs and None<T> in others. Other languages have tried to do that and my impression is that it just ends up a mess because you get into situations like in Java where you have a variable of type Option<T> but it that could still also potentially be null because every reference can be null in Java. Ick!

I'd rather give users a consistent good experience working with null as the absent value everywhere and do the best we can in the relatively few cases where it doesn't work well.

@lrhn
Copy link
Member

lrhn commented Jan 7, 2019

So then is foo<T>({T x}) { foo<T>(); } disallowed?

That .... would probably be disallowed, yes. It's not statically known that T is nullable. And
foo<T>({T x}) { foo<T>(x: x); } would be allowed, as would foo<T>({T x}) { foo<T?>(); }.

So, a call site with no argument is statically invalid if an optional argument is not passed for a parameter with a type that is not known to be nullable and which has no default value.

That means that we will need three different function types Function({int? x}), Function({int x=}) and Function({int x}), where the last one has no default value and the middle one has a default value (the first one always has a default value, it may be null). Only the first and second are optional arguments.
Ot we can say that the middle one is the same as the first one, Function({int? x}), and passing null explicitly or implicitly (by omitting the argument) triggers the default value.

In the foo example, the Foo<T>() invocation has T unbounded. All we know is that T extends Object?, so all we know about foo<T> is that it is a super-type of Function({Object? x}). The function parameter may be non-nullable, so it can't be omitted.

@eernstg
Copy link
Member

eernstg commented Jan 7, 2019

@munificent wrote:

I'd rather give users a consistent good experience working
with null as the absent value everywhere and do the best
we can in the relatively few cases where it doesn't work well.

+1!

So let's return to null and optional parameters; @lrhn wrote:

[we could make it] statically invalid if an optional argument
is not passed for a parameter with a type that is not known
to be nullable and which has no default value

This is basically the same thing as proposing that a named parameter with unknown nullability should be treated (with respect to its optionality) as if it were non-null.

I think that might work, but I'd still prefer the approach where we simply outlaw a named parameter with no default that has a type with unknown nullability (so you must write X! or X?), because it's a relatively tricky case to leave implicit:

  • You'd typically determine whether a named parameter is required by looking at it ({int x} is required, {int? x} and {int x = 0} are not).
  • When the type is a type variable whose upper bound is non-null you have to look at the declaration of that type variable ({X x} is required because we have X extends int).
  • But when the type is a type variable whose upper bound is a top type or another nullable type, it's still required, because the value of the type variable could be a non-null type ({X x} is required even though we have X extends int?, because X could have the value int).

I'm just saying that I'd prefer to avoid the need to explain that we also have the third case. ;-)

That said, we'd need syntax for required named parameters in function types anyway (say, void Function({required X x})), so we might as well allow the same syntax in a function declaration, too, and then it could be up to a linter to say that all required named parameters must have the modifier required.

@munificent
Copy link
Member

That said, we'd need syntax for required named parameters in function types anyway (say, void Function({required X x})), so we might as well allow the same syntax in a function declaration, too, and then it could be up to a linter to say that all required named parameters must have the modifier required.

That's an excellent point.

@Cat-sushi
Copy link

I'd like to know the difference between function declarations and function types.

@munificent
Copy link
Member

A function declaration creates an actual executable unit of code that has a
handle you can get a reference to. That also means there is a place—the prelude of the function—where the runtime behavior of optional parameters and default values can come into play.

A function type is just a type annotation, a thing the type checker can reason about. There's no concept of a default value in the static type system, because the type checker doesn't care about them. (There are "static checks" around default values, but they aren't part of the type system properly.) That means there's no way to write a default value in a function type annotation, because it wouldn't mean or do anything.

@munificent
Copy link
Member

Then when you write var y = array[i] the type of y is inferred to be nullable (b/c Object is nullable, according to our premise), though it can clearly see that the array doesn't contain any nulls.

Why is that a problem? The fact that the type system doesn't know if it contains null doesn't prohibit any operations. Since the only methods Object supports are also defined on null, the set of methods you can call on element of the list are the same either way.

Further, allowing Object to contain null allows more operations on that list: it means you can add null or elements of other nullable types to it.

But most likely, you don't want JSON to include null values.

Maybe, but JSON does allow null. It's reasonable to want to prohibit it in cases where you know there shouldn't be a null, but it's not a problem for JSON itself.

When reading JSON, the object's type would have to be Map<String, Object?> to reflect that the type system doesn't know if there is a null in there or not. In that case, it may be more painful to have to cast away that nullability just to the result in a variable of type Object.

@lrhn
Copy link
Member

lrhn commented Feb 4, 2019

@eernstg wrote:

That said, we'd need syntax for required named parameters in function types anyway (say, void Function({required X x})), so we might as well allow the same syntax in a function declaration, too, and then it could be up to a linter to say that all required named parameters must have the modifier required.

I'm not sure we do need the required flag. Just saying "non-nullable means required" works for function types too. The way I'd handle default values is (as usual) to make the parameter type nullable, and always convert null to the default value.
So, if you write

int foo({int x = 42}) => x;

then the function type of foo is int Function(int?).

With type variables, we might not know whether a parameter accepts null, and then we have to assume that it doesn't. That's not new, we also don't know whether it accepts a num, and we have to assume it doesn't. The only thing we can safely pass to a parameter type of T is something known to be type T, whatever it is.
You can write a function type like void Function(T? x) where the parameter is known to be nullable, no matter what T is.

Still, this all hinges on making null mean the same as omitted, because then a default value for a non-nullable type will handle both cases.

@Cat-sushi
Copy link

int foo({int x = 42}) => x;

then the function type of foo is int Function(int?).

I feel it's very confusing that int is deemed as int? just because it is optional, even if it has default.

@lrhn
Copy link
Member

lrhn commented Feb 4, 2019

I do fear that it will be confusing that {int x} means non-nullable and {int x = 42} means nullable (outside the function, inside the function both are non-nullable). If we did not allow {int x} because the argument isn't actually optional, then it would be easier to get used to because every type inside {...} would then be nullable. Having to look ahead to see the default value makes it harder to read.

In practice, the API docs will write the external type, and not the default value, so users shouldn't see the non-nullable type. It's only the API author who can get confused, and they are the ones relying on the variable being non-null internally in the function.

I actually think it can work, but I would definitely want a usability study of it before making a final decision.

@Cat-sushi
Copy link

Cat-sushi commented Feb 4, 2019

I think,

  • nullability outside function and inside function should be same.
  • nullability should be simply specified by ?.

Consequently, non-nullable named parameter without default must be required, with/ without required annotation.

@eernstg
Copy link
Member

eernstg commented Feb 4, 2019

@lrhn wrote

Just saying "non-nullable means required" works for function types too.

I think int Function({required int x}), which can be the type of int foo({int x}) => x; is better documentation than anything which doesn't a hint like the word required.

Besides, your examples focus on the case where the parameter is actually not required, because there is a default value. We could of course just require a default value for all named parameters with a non-null type, but I would expect the notion of a required named parameter to be useful in practice, which means that we might as well consider supporting it, now where it seems to arise naturally anyway.

@ds84182
Copy link

ds84182 commented Feb 5, 2019

I think whether or not a null means default value should be the choice at the call site. I find the proposed behavior confusing.

Thinking about something like foo(x:? bar), where x would be omitted/replaced with the default value if bar is null. This can also apply inside of map literals, for consistency {"omit_me_if_null":? someValue}. Of course, the syntax should probably be refined, but the general point is that the caller should be the one to decide if the parameter is omitted when the value is null. Otherwise, I'd like an analyzer lint.

We can also have something more intuitive, like foo(x: bar ?? default). Default is a reserved identifier, iirc.

@lrhn
Copy link
Member

lrhn commented Feb 5, 2019

@lrhn: what are your arguments against "Nothing" (or equivalent word)?

I'm assuming you refer to the example above (so how did you know that I'm against it? 👿 :).

My opposition to that use of Nothing is that it puts us in a position where we have two ways to have no value: null and Nothing.
You should have either zero, one or an infinite amount of any feature. Not two.

As I read it, using Nothing as the "value" of an expression in a parameter position means not passing that parameter. Named only, or can you do f(1, test ? 2 : Nothing, 3) when f has type Function(int, int, [int])? (Emphatic no, I don't want what is lexically the third argument to become the second depending on the run-time value of an expression). So, it's only for named parameters (and likely for list/set literal elements, which can be elided safely) that Nothing makes sense at all.
What is the static type of foo ?? Nothing? Is it Foo or Foo|Nothing? Can you write test ? foo ?? Nothing : bar? If so, you do need to know the type of the sub-expression. So, you introduce a new entity into the type system, and we need to figure out where it belongs, even if we don't allow you to write the type as a function return type.

And that still means that you can only use it locally, you cannot abstract over it. No

int|Nothing helper(condition, value) => condition ? value : Nothing;  // or more complex,

Otherwise it goes everywhere in the type system, and we'll have Notingable types like we have nullable types, and need syntax to describe them.

That's a very, very big impact for such a specialized feature. I am opposed to very specialized features that are not also very specifically scoped, because they are typically a sign of digging yourself deeper into an existing a bad design. It's "one more feature, and then things will be good", but in practice there is always one more corner case that needs handling, and now you have n+1 features that interact badly.

If it doesn't generalize, then it's not worth it. Take a step back and see if there is a simpler thing that solves more problems instead. (Sometimes you need to go so far back that it's not realistic, sadly).

If it does generalize ... then it's still a second Null, just with a slightly different behavior in some cases. Are we really better off than with one Null, or have we just increased the number of cases that clients have to care about?

So, lets look another design for the same thing; optionally passing a non-null value (like @ds84182 just wrote above).

foo(bar?: value);

which is equivalent to foo(bar: value) if value is non-null and to foo() if value is null.

That's also a very specific feature, but it is localized. It doesn't affect just any expression, it only affects one named parameter being passed. The syntax only applies to named parameters, so it doesn't bleed anywhere. It uses the existing null value to signal absence, and doesn't affect the type system at all.
I could see that being possible (and not bar ?? default because that again makes default look like an expression, and then we are back in the type-system issues).

@munificent
Copy link
Member

This is one of my gripes with if-element:
[ if (cond) 15 else 20 ] means the same as [ cond? 15 : 20 ];

Sure, lots of features have some overlap like this. List<int>() means the same thing as <int>[] but the former also lets you do List<int>(10), which the latter can't express, and the latter lets you do <int>[1, 2, 3] which the former can't express.

Orthogonality is a goal but it's never perfectly attained.

A conditional expression can be used inside a key or value in a map:

{
  (condA ? "key1" : "key2") : (condB ? "value1" : "value2")
}

Which is something an if element cannot express. But an if element can do:

[
  if (condA) ...someList,
  if (condA) noElseClause
]

Both of which can't easily be expressed using ?:.

if-element also introduces the second (we don't like the number 2, remember? :) notion of "generator";

It is syntactic sugar on top of the same Iterator protocol used by for-in, addAll(), sync*, etc. It doesn't introduce a new notion of generators, it introduces a new place you can use them:

Iterable<int> countToTen() sync* {
  for (var i = 1; i <= 10; i++) yield i;
}

void main() {
  var list = [...countToTen()];
  for (var i in list) print(i);
}

Here we use a sync* method to produce an Iterator which gets consumed by a spread to produce a list which in turn is iterated over using a for-in loop. All of these can be freely mixed and matched because they all use the same Iterator protocol.

@eernstg
Copy link
Member

eernstg commented Feb 8, 2019

I suspect that at this point we could conclude that Object is a non-null type, and the top type (that has no special "bits" like dynamic and void) is designated as Object?. @leafpetersen and @lrhn, do you agree?

@leafpetersen
Copy link
Member Author

My inclination is that if we have the ! type operator, then Object is nullable and you write Object! if you want the non-nullable version; and if we don't have the ! type operator, then Object is non-nullable, and you write Object? if you want the nullable version.

@eernstg
Copy link
Member

eernstg commented Feb 11, 2019

@leafpetersen wrote:

My inclination is that if we have the ! type operator, then Object is
nullable and you write Object! if you want the non-nullable version

OK, but that would also give us a quite nice and consistent setup: We can express both the top type (with no special treatment like dynamic and void) and its non-null sibling, and that was my main concern anyway. It also directly reflects the actual interface properties (null does have a toString(), etc., cf. #175). Looks like we could at least establish a conditional decision. ;-) @lrhn?

@lrhn
Copy link
Member

lrhn commented Feb 11, 2019

I think I would want Object to not be nullable, and Null and Object both be subtypes of a synthetic anonymous "top" type which declares the shared interface, but which there is no way to denote in a program.

Object is just a normal interface type, so it's confusing if Object is nullable and Object! is not, but int is not nullable, and you have to write int? to be nullable. That inconsistency is not buying us anything. We will have exactly the same types in the type hierarchy, just with an inconsistent notation.
The one thing a nullable Object gives us is an explanation for what null is. I can live with saying that it's an object (lower case), and its class Null has no superclass, just like Object. Both still implement the "default interface of all Dart objects", which is no longer just the interface of Object.

@eernstg
Copy link
Member

eernstg commented Mar 14, 2019

@lrhn, @leafpetersen, are we gravitating toward the conclusion that Object will be a non-null type?

@munificent
Copy link
Member

If Object is non-nullable (which I'm fine with) and there's no name for the top type above Object and Null, then what does the error message look like here:

class Foo<T> {
  method(T x) {
    x.oops(); // <--
  }
}

I'm worried by the idea of a type that has a method set, and that is used by the static type checker for producing errors, but that doesn't have a meaningful name that we can show users. Does this say something like "'Object?' does not have a method 'oops()'."?

My suspicion is that the tools will have to keep coming up with their own weird ad hoc ways to refer to this thing. Maybe we should just name it.

@lrhn
Copy link
Member

lrhn commented Mar 14, 2019

Object? is a name for the top type above Object and Null, so the message will likely say "Object? does not have a method oops()".
I don't think we'll make a non-bounded type variable be dynamic, so that's the only option.

@eernstg
Copy link
Member

eernstg commented Mar 15, 2019

Have you considered letting the omitted bound mean Object rather than Object?? This would mean that a lot of code would get the non-null discipline "for free", and the locations where you really want to allow a type variable to be a nullable type you'd have to ask for it.

@lrhn
Copy link
Member

lrhn commented Mar 15, 2019

I don't believe we'll ever get "migration for free", so doing using Object as the default bound is probably just going to be confusing. If I have no bound, why are there types I can't use?

(I'd totally go for a model where Object was a supertype of Null and you couldn't use a ? type as a bound, but that's not what we're aiming for :)

@eernstg
Copy link
Member

eernstg commented Mar 15, 2019

Makes sense, but the rationale could still be "if you want your local variable x to be nullable you'll need to add ? to its type; if you want your type variable to be nullable you'll need to include the ? as well, so if it doesn't have a bound you'll have to write Object in order to have a place to put it".

And here's an argument against my proposal (letting the omitted bound be Object): Maybe non-null type variables without a bound aren't useful very often, because (1) we do allow usage of the Object interface on a receiver whose type is nullable, and (2) you can't do anything else with a receiver of type X when X is a type variable with no bound. So who cares? ;-)

I think, now, that the omitted bound should mean Object?.

@eernstg
Copy link
Member

eernstg commented Mar 21, 2019

@leafpetersen, @munificent, @lrhn, do we have a decision that "Object is a non-null type" such that we can close this?

@leafpetersen
Copy link
Member Author

Decided, Object is non-null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests

7 participants