Skip to content

NNBD: passing optional non-nullable parameter #39897

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

Closed
ghost opened this issue Dec 21, 2019 · 6 comments
Closed

NNBD: passing optional non-nullable parameter #39897

ghost opened this issue Dec 21, 2019 · 6 comments
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality

Comments

@ghost
Copy link

ghost commented Dec 21, 2019

Not sure where to post it, maybe it's the intended behavior or a bug or an open issue, but ... this doesn't work:

func({int x=0})=>0;
void main() {
  func(x:null); // The argument type Null cannot be assigned to the parameter type int
}

If this is by design, it's very problematic. E.g. we may have a map of 'key:value' pairs and want to create an instance of class Foo by calling the constructor Foo(foo: map['foo'], ...20 parameters), but as soon as there's a single non-nullable parameter, we won't be able to do that - at least, it's not obvious how.
This would be equivalent to the invitation to declare all optional parameters nullable.

Pretty sure this problem was discussed somewhere, but I couldn't find any mention of it in the NNBD spec.

(Maybe it's just a bug, and passing null is allowed as a token for default value?).

EDIT: the proposal of null-elimination operator might help address the issue, but the status of the idea is unclear at the moment.

@devoncarew
Copy link
Member

I think this is working as intended - if the function wants to accept null as a potential value, then the type of x would need to be declared as int?.

cc @leafpetersen to confirm

@devoncarew devoncarew added the type-question A question about expected behavior or functionality label Dec 28, 2019
@lrhn
Copy link
Member

lrhn commented Jan 2, 2020

In NNBD, all optional parameters must either have a default value (which is assignable to the parameter's type) or be nullable (which means that it has a default default-value of null). That way, the function can keep operating if you do not pass a value for the parameter.

You can only pass arguments which match the type of the parameter. That means that you cannot pass null as an argument to the func written here because it needs an int. You can omit passing an argument, in which case the function will get the default value instead.

Passing null will not trigger the default value, it didn't before NNBD, and it won't with NNBD. If the parameter is not nullable, passing null is no longer allowed, which is what NNBD is all about.

It does mean that you cannot easily call a function with computed arguments when some of them should be omitted. Not without knowing writing the default value at the call point:

Foo(foo: map["foo"] ?? 1, ... qux: map["qux"] ?? "defaultString");

That is not new though. If the function uses non-null default values now, it also doesn't work to pass null as argument. The function will have to accept null and then do a ?? defaultValue internally. It can keep doing that, in which case the parameters will be nullable.

So, there really isn't any change here except that you have to write a ? if a parameter accepts null. If the function currently throws when it gets null as argument, it should change to a non-nullable parameter type so the caller can get the static error.

@lrhn lrhn added the closed-as-intended Closed as the reported issue is expected behavior label Jan 2, 2020
@ghost ghost closed this as completed Jan 2, 2020
@lrhn
Copy link
Member

lrhn commented Jan 8, 2020

@tatumizer It's a cute idea, but it doesn't really work the way you write it. First of all, a value cannot have a static type. Values have run-time types, expressions and variables have static types.
So, for nullableExpr ?? useDefault, the expression we are passing is the entire thing, which has the same static type as nullableExpr. The run-time value might be null, but we cannot track that back to the second part of the ?? operator to find its original static type.

@lrhn
Copy link
Member

lrhn commented Jan 13, 2020

Taking it further!

We could allow default in any tail-position expression of an optional argument expression.

An expression e is in a tail position of an expression b if

  • it is the expression b itself,
  • b is (c) and e is in tail position in c.
  • b is e1 ? e2 : e3 and e is in tail position in e2 or e3.
  • b is e1 ?? e2 and e is in tail position in e2.
  • b is e1 || e2 or e1 && e2 and e is in tail position in e2 (and must have type bool)

That means that if default occurs, it occurs in a position where its value is only used as the argument value, and then we can treat it as an unpassed argument instead.

It's grammatically specifiable, we just have to have dual production hierarchies: <expression> and <tail-expression> where only the latter can produce default, and similarly all the way down. Annoying, but definitely doable (and if you have parameterized productions, like <expression>[tail=false] then it's even simpler.

@lrhn
Copy link
Member

lrhn commented Jan 15, 2020

Maybe we should allow (if (cond1) 0) as an element expression.

@idkq
Copy link

idkq commented Feb 12, 2021

You could use maps

class Dog{
  final String name;
  final int age;
  Dog({this.name, this.age});
  
  Dog.fromMap(Map<String, dynamic> map): 
    name = map['name'] as String, age = map['age'] as int;
  Map<String, dynamic> toMap() => {'name': name, 'age': age};
  
  Dog copyWith(Map<String, dynamic> fields){
    var newObj = this.toMap();
    for (final field in fields.entries)
      newObj[field.key] = field.value;
    return Dog.fromMap(newObj);
  }
}

void main() {
  Dog buddy = Dog(name: 'Buddy', age: 12);
  Dog puppy = buddy.copyWith({'age': null});
  
  // OUTPUT=> Name: Buddy Age: null
  print('Name: ${puppy.name} Age: ${puppy.age}');
}

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

3 participants