Sounds promising \o/
Defining optional parameters without default as nullable wouldn't bother me - just my 2c.
- Having to explicitly annotate optional parameters without default values as nullable feels tedious:
method([int o]) // <-- Error.
method([int? o]) // <-- OK.
We may want to implicitly consider the parameter type nullable in that case, but that feels a little weird too. I don't like the type being something different from what you wrote. I think we'll have to do some exploration here to get to a usable syntax.
f(int i, [int j]) => ...
g(int? m, [int? n]) => ...
f(null) // warning: 1st argument should be non-null
g(null) // ok
f(0, null) // warning: 2nd argument should be non-null
g(0, null) // ok
Hi Bob. Great progress, esp. for someone battling on other fronts. Sorry that you aren't feeling well, and I hope you get rid of that cold soon!
- Having to explicitly annotate optional parameters without default values as nullable feels tedious:
method([int o]) // <-- Error.
method([int? o]) // <-- OK.
We may want to implicitly consider the parameter type nullable in that case, but that feels a little weird too. I don't like the type being something different from what you wrote. I think we'll have to do some exploration here to get to a usable syntax.I came to the conclusion that no matter which syntax and semantics one adopts for optional parameters, there is always an initial feeling of weirdness :)
That being said, consider the following declarations:
f(int i, [int j]) => ...
g(int? m, [int? n]) => ...Under NNBD, the declaration of i allows the analyzer to report issues at points of call:
f(null) // warning: 1st argument should be non-null
g(null) // okI believe that the declaration of j should have the same interpretation:
f(0, null) // warning: 2nd argument should be non-null
g(0, null) // ok
Of course, from within the body of f, the type of j is nullable int.
f([int i]) {}
What is nice about this interpretation is that when j is null inside f, we now know that it is because no argument was provided (as opposed to the caller having supplied a value of null). Without such a nullity semantics, we cannot make that distinction.
What I've outlined above is the gist of the dual-view semantics proposed in Section E.1.1 of the DEP. More cases are covered there.
f([int i]) {g(i);}g([int i = 0]) {print(i);}g(); // 0f(); // null
f([int i = 0]) {print(i);}// same as:f([int i]) {i ??= 0;}
We've considered changing that so that an explicit null also means "use the default value, if any".
I wasn't aware that that language feature was up for review.
That would be a wonderful change! (I recall folks asking for this a while back.)With that in place (or at least the hope of that being adopted :), I would completely agree with your proposed semantics for optional parameters.
It seems "lateinit" is a logical necessity (however ugly):https://kotlinlang.org/docs/reference/properties.html, lookup "lateinit"
If "lateinit" is supported, then optional parameters can be treated as "leteinit" by default (unless explicit default is provided)
--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.
Consider the analogy with "final".You can declare an instance variable like "final int x=0", and later remove "=0" (relying on constructor to initialize it) - without triggering any warnings, or need in extra markup like "lateinit"It's the same situation.
Coming from different angle: suppose you provide a library for others to use, and people are looking at your method declarations. They don't want to see "lateinit infoo([lateinit int x]) - because "lateinit" doesn't add any information to THEM. From library writer's viewpoint, annotation this might serve some (minimal) purpose, but not for the reader.
I think the least disruptive approach would be really treat them all as lateinit by default, but whenever compiler cannot prove that initialization really happens before use, require exclamation mark whenever the variable is accessed (exclamation mark triggers explicit runtime check for null). E.g.int x;... x may be initialized after declaration, but compiler cannot prove itint y=x! // need to explicitly invoke runtime checkIf you don't put exclamation mark there, compiler will complain - at every point of access.
int x;... x may be initialized after declaration, but compiler cannot prove itint y=x! // need to explicitly invoke runtime checkIf you don't put exclamation mark there, compiler will complain - at every point of access.Seems like you can either use another variable or extract a helper method?For lazy initialization, you might have something like this:Foo ensureFoo() {_foo ??= new Foo();return _foo;}
Foo ensureFoo() {_foo ??= new Foo();
return _foo ??= new Foo();}
Foo? _foo;Foo get foo => _foo ??= new Foo();
I'd be interested in hearing what initialization patterns Bob ran into in dart_style.
Looking at this lateinit, I wonder if something similar could work with final. Where this would mean:`lateinit final int i;`That "i" can only be initiated once, to a non null value. I often have to rely on factory contructors to just get my fields final.Using lateinit seems cleaner, and that could even work outside of the constructor right?
> ... but it implies that every access of the field has a runtime check to see if it was initialized first, so there's a perf cost.I think this cost is negligible. The branch will always be predicted correctly, so we are talking about 1 cycle, and even that is in a pipeline (so it's a fraction of the cycle).
There's also a way to do it in hardware - e.g. use "test addr" instruction, and set things up so that null corresponds to non-existing addr. This will save a bit of space in instruction cache and branch prediction cache.(Though it depends on OS and H/W architecture, so the trick might, or might not, be feasible).Dart has much more freedom to do tricks on a low level than Kotlin (b/c JVM).Runtime checks are necessary anyway if you want consistent behavior on lists of non-null values: compiler will have a hard time figuring out (statically) what is initialized and what isn't.
--
If the compiler can determine that a variable is initialized before use on all paths, it can drop initialization checks.The question is then whether we want to make programs where that is not the case into errors.
That would require the spec to specify some sort of flow analysis to detect "definitely initialized variables" (only matters for non-nullable ones without initializers).
That also means that a lazily initialized non-nullable variable is effectively a nullable variable, just one where you are not allowed to assign null to it, and where it throws on read if it's null, instead of on use. I'm not sure I want that extra kind of variable.
Instead I'd probably prefer to just require that the author makes the variable nullable, because it is. The fact that it has some life-time where it isn't null, and that overlaps with the code where you actually use it, is not specific to variable initialization. It can happen for any nullable variable that you ensure its non-null-ness, and then use it as non-nullable.
E.g.:if (x != null) {...}orassert(x != null);x as Foo; // non-null Foo....Both of these should eventually type-promote x to non-nullable (I hope).
The other option is to require initialization. That's easy for primitive values, because you can always initialize to 0 or false or "", but for more complex classes, you'll effectively have to have a null-object for the type. That's likely as big an overhead for the user as allowing null.
I assume this is mainly for instance variables or global variables.For local variables, it's easy to just declare a new variable:var notNullX = x ?? notNullValue;or just type promote with a single check.notNullValue as NotNullType;
I'm sure there are cases with loops and distinct initialization in if-branches that makes that problematic, but you can just make it nullable if the variable really is null.
foo(int? i) {if (blah) {i = 1;} else {if (i == null) throw "WAT";}i.length; // <-- OK, promoted here.}
For instance and global variables, it is extremely hard to detect that a variable is initialized before use.For instance variables, the one chance is to see that it leaves the constructor as non-null.
The workaround I would use there is to not initialize it in the body, and instead use two constructors, the initialing one and the one that does dependent computations.
(If there isn't two fields that need values depending on the same computation, you can just do:Foo(x, y) : z = _compute(x, y)The problem is when you need to do something likeBar() : _x = new Completer, y = _x.future;There you need an extra constructor:Bar() : this._(new Completer()); Bar._(this._x) : y = _x.future;(That's the solution to needing local variables in initializer fields).
I don't think I'd like the user to have to write something at the declaration to allow a partially non-null variable. Even if it's as simple as `int! x; // lazily non-null`.
...
I don't think I'd like the user to have to write something at the declaration to allow a partially non-null variable. Even if it's as simple as `int! x; // lazily non-null`.
Yeah, I'm not crazy about it either, but it does seem to come up a bunch. You may wish to check out the nnbd prototype branch and try getting some code null-clean yourself to get a feel for it.
Cheers!– bob
In case you're interested in some numbers (from the Java world) to qualify "it does seem to come up a bunch" ...
in Appending I of DEP-30, we reference an empirical study of 700MLOC of Java code (Chalin et al., 2008): almost 60% of "nullable" declarations were monotonic non-null (i.e., eventually/lazily non-null) -- see Table 4 on page 25 of the cited paper. Of course, that was for Java, but I would not be surprised if the numbers were similar for Dart.
class B {C x; // Initially null, but assignments cannot make it null.
foo() {
if (x != null) {
... // Do whatever you want.
x.baz(); // `x` is definitely still non-null.
}}
bar() {
x.baz(); // Subject to a dynamic null check. The linter could flag it.
x.qux(); // `x` is definitely still non-null.
}
}
– bob
--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.
– bob
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+u...@dartlang.org.
--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.
SGTM. In fact, we did somewhat similar analysis in JML (under the assumption that a program was single-threaded).A bit of trivia for language buffs (because this isn't really relevant to Dart). In the context of multithreaded Java programs, you need to make a local copy of a field's value when you want to use the value after the nullity test. Interestingly, Eiffel introduced special if-statement syntax for such cases (combining a local variable declaration and an if-condition -- i.e. Fig. 7 is a Java for the Eiffel in Fig. 8:
if (e is T x) {
... // `x` is in scope, with type `T`, final, initialized to the value of e.
}
// But `is` is a boolean which may not fit, so maybe we want an `as` variant?
e as T y; // .. very similar to `T y = e`, but shows intent to re-type `e`.
// And then the nnbd stuff. Maybe we could do this?
if (e is! Null x) {
... // `x` in scope, final, with typeof(e)-but-non-null, init. to `e`.
}
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.
--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.
On Tue, Feb 28, 2017 at 6:27 PM, Patrice Chalin <pch...@gmail.com> wrote:SGTM. In fact, we did somewhat similar analysis in JML (under the assumption that a program was single-threaded).A bit of trivia for language buffs (because this isn't really relevant to Dart). In the context of multithreaded Java programs, you need to make a local copy of a field's value when you want to use the value after the nullity test. Interestingly, Eiffel introduced special if-statement syntax for such cases (combining a local variable declaration and an if-condition -- i.e. Fig. 7 is a Java for the Eiffel in Fig. 8:Interesting! In a similar vein, we have discussed the following syntax several times (it's not necessarily going to happen, but we are aware of this idea):if (e is T x) {... // `x` is in scope, with type `T`, final, initialized to the value of e.}// But `is` is a boolean which may not fit, so maybe we want an `as` variant?e as T y; // .. very similar to `T y = e`, but shows intent to re-type `e`.// And then the nnbd stuff. Maybe we could do this?if (e is! Null x) {... // `x` in scope, final, with typeof(e)-but-non-null, init. to `e`.
(Then question remains under what conditions compiler generates a warning, but it's a different question).