Skip to content

Provide better support for default function parameters #2899

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

Open
GregoryConrad opened this issue Mar 10, 2023 · 6 comments
Open

Provide better support for default function parameters #2899

GregoryConrad opened this issue Mar 10, 2023 · 6 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@GregoryConrad
Copy link

GregoryConrad commented Mar 10, 2023

I looked through some other issues, but (to my surprise) did not find what I am looking for.

TL;DR: I am looking for syntax like the following:

void fn<T>({
  // default here is a common super type for optional function parameters
  T optionalValue = default,
}) {
  if (optionalValue == default) { // or maybe "is default"
    // ...
  }
}

Nullable default parameters work great until you have a type T that is nullable that you need to pass into a function.
Dart has no support for such a situation.

There are two workarounds (at least that I am aware of), both of which have some problems.

  1. Create a private const default parameter object, and then use an abstract method to override the parameter type:
class _DefaultParameter {
  const _DefaultParameter();
}
const _defaultParam = _DefaultParameter();

abstract class A<T> {
  void fn({T myValue});
}

@internal
class B<T> extends A<T> {
  void fn({Object? myValue = _defaultParam}) {
    if (myValue is T) {
      // myValue was explicitly set
    } else {
      // myValue is default
   }
  }
}

But this forces you into inheritance and some really ugly generic work. What happens if you just need a function? Perhaps you can use a callable class, or a function typedef, but both of those require another workaround too.

  1. Use an Option or similar class from functional programming.
void fn<T>({Option<T> myValue = None()}) {
  if (myValue case Some(:data)) {
    // use data.
  }
}

However, this requires users to explicitly wrap the parameter in Some(myValue), and requires a dependency on some Option implementation.

Each of these workarounds has drawbacks.
Any sort of copyWith method (amongst many others) run into this issue when handling nullable members, and is forced to implement one of the above.

Perhaps this proposed default super type would act similar to null, with a builtin type union?

@GregoryConrad GregoryConrad added the feature Proposed language feature that solves one or more problems label Mar 10, 2023
@Cat-sushi
Copy link

IMHO, #2232 is better.

@GregoryConrad
Copy link
Author

GregoryConrad commented Mar 13, 2023

@Cat-sushi I may have easily missed something in that issue while skimming over it (the issue is related, thanks for linking it), but there seems to be no way to pass null to a nullable parameter with no default explicitly defined? I.e., you won't be able to distinguish between null that was passed in and the null that is a lack of an argument.

@Cat-sushi
Copy link

#2232 is not a alternative, but a counter-proposal of this issue.

there seems to be no way to pass null to a nullable parameter with no default explicitly defined? I.e., you won't be able to distinguish between null that was passed in and the null that is a lack of an argument.

No, we can't , and I think we shouldn't.

@GregoryConrad
Copy link
Author

and I think we shouldn't.

Here's an example where #2232 breaks (as far as I can tell) and you're stuck with both of the above workarounds I mentioned. I am encountering this issue right now, and thus why I opened the issue.

Say you are designing a package with an API represented as an object that is given to package consumers:

final class Api {
  // ...
}

And that you can compose extensions based on the API that give it some new concrete functionality (based on everything given in the API):

typedef ApiExtension<R> = R Function(Api);

Now say that someone (maybe in an entirely different package) wants to extend this API with an extension, like allowing the API to react to Future<T> and Stream<T>.
The ApiExtension for that is easy enough:

typedef FutureWatcher = SomeValueWrapper<T> FunctIon<T>(Future<T>, {T initialData});
FutureWatcher watchFutureExtension(Api api) { ... }

Now, take a peek at the initialData above. It isn't required, but T might also be nullable. Here, you're stuck with a modification of the first workaround I gave above. There's no way to know if the user wanted initial data set to null or if there was no initialData at all!

But now, say it is custom in the package to provide extension methods on something like Api so that the extensions are easier to use. We can do that like so in any other package:

extension FutureWatcher on Api {
  SomeValueWrapper<T> watchFuture(Future<T> future, {Option<T> initialData}) { ... }
}

Here, it is an extension method so we can't even do the default object approach! Instead, we force users to pass in an explicit value for Option whenever they want to use the extension method.

With the current state of default parameters, you have issues like this where resulting APIs are going to be broken, because null can semantically mean something completely different than a default.

@GregoryConrad
Copy link
Author

If we had union typing, as per #2232 (comment), then this would be a non-issue. However, we don't have union typing.

@GregoryConrad
Copy link
Author

GregoryConrad commented Mar 13, 2023

I think the issue with #2232 really just boils down to the fact that null semantically does not mean "lack of" all the time, and can have some other attached meaning.

From #2232:

If you can definitely pass null as an argument, you can also omit the argument instead.

Those are inherently different scenarios, at least when thinking about generics/user-specified types that as a package author have no control over.

I do, however, like the idea behind #2232 with the better function parameter grammar, but the way to do so should not be through null in my eyes; it should be through a different special type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

2 participants