-
Notifications
You must be signed in to change notification settings - Fork 213
Reference parameters #1911
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
Comments
|
My two cents: Make the |
We would probably want both This is definitely a thing we could do but, overall, I'm not convinced it carries its weight. I haven't found myself missing ref params much in Dart. The few times I do, it's usually just to have multiple return values, which could be more directly handled by support for tuples. |
But ... It's true that we have gotten this far without reference parameters, and that they probably have some risk of misuse, or uses that are just covering for the feature we really want, like multiple return values. I do think it could help with code-reuse in some places where we currently require you to rewrite the code (or introduce the get/set closures, which nobody does). Say set foo(Foo value) {
var current = _foo;
if (!identical(value, current)) {
_foo = value;
_fooChanges.add(ChangeNotification(current, value));
}
} could become: set foo(Foo value) => ChangeNotifier.updateWithNotification<Foo>(&_foo, value, _fooChanges); everywhere you have that pattern, and depend on a single: static void updateWithNotification<T>(T? target, T value, StreamController<ChangeNotification<T>> notifier) {
var current = target;
if (!identical(current, value)) {
target = current;
notifier.add(ChangeNotification<T>(current, value));
}
} So, the feature has expressive power, even if it can be desugared to existing Dart code, because it can make code-reuse shorter and practical (and the desugaring to closures can easily end up being more verbose than the original). (And introducing the notion of a LHS-abstraction would make the specification easier, we can define |
We've talked recently about statement-level or expression-level metaprogramming through something like inline functions or compile-time execution. I wonder if we could subsume this feature under that. If we had something like inline functions where the parameters became transparent references to the original arguments, it might be possible to cover these use cases. |
Macros receive the parameter expression as unevaluated syntax. You could write a |
Hence the recommendation to use |
That looks like class Ref<T> {
T? value;
Ref([this.value]);
} with more steps. A more complete version could be: final class Ref<T> {
({T value})? _value;
Ref(T value) : _value = (value: value);
Ref.empty() : _value = null;
bool get hasValue => _value != null;
T get value => (_value ?? (throw StateError("No value"))).value;
set value(T value) { _value = (value: value); }
} It can distinguish a value of |
Dart currently only allows passing arguments by value (where object references are values).
However, the language has first class closures which can close over variables, so you can effectively pass a reference to a variable by passing a getter function and a setter function:
It's not particularly convenient, but it does work.
So, Dart could introduce reference parameters without actually needing to extend the power of the language, just the expressibility.
Say:
where
int&
/ref int
is a reference to a mutable integer variable. Afinal int& counter
/final ref int counter
parameter would be immutable reference to a final or mutable variable.(Or we can even say
in int counter
for read-only,out int counter
for write-only andinout int counter
for either, if we dare reuse variance notation for variables.)A reference parameter must be passed a suitable left hand side as argument. Either a variable (local, static, instance) or an index operator access — the things that can currently be assigned to. A final variable cannot be passed to a non-final reference parameter.
The local reference variable is then an alias for the left-hand side that was passed to it. Reading from the reference variable will read from the underlying variable. Writing to the reference variable will write to the underlying variable. A reference variable can be passed as value to another reference parameter.
(We can perhaps even declare reference variables (aliases), like
int& x = y;
, and a reference variable must be initialized eagerly, because initialialization is different from assignment, which assigns to the underlying variable. Or it might just be too confusing, and we should just restrict it to parameters).Function types will distinguish reference paramaters from non-reference parameters. Neither is a subtype of the other, so
void Function(int)
andvoid Function(int&)
are unrelated types. Technically, we could probably allowvoid Function(int)
to be a subtype ofvoid Function(int&)
, but it means the dynamic calling semantics must check which one it is to figure out whether to pass the variable by reference or by its value. It's better to just distinguish the two.Effectively, it is as if there are hidden class types
Ref<T>
andFinalRef<T>
(orInRef
/OutRef
/InOutRef
if we want full generality), and invocations of a function with aRef<T>
parameter will implicitly create theRef
instance for you, capturing the getter/setter functions into a structure like the one shown above. And that would be a possible implementation strategy, although I hope implementations can do better.That makes references a type just like any other type, just a hidden type with special methods that can't be accessed directly, with implicit creation of the value where needed.
Usage
With reference parameters, we can introduce helper functions like:
which allows code which reads better by abstracting over a sub-part of an algorithm, even if it affects local variables:
or
This allows you to have user-written operations like the composite operations that are currently language-only,
+=
,++
,??=
, etc.(That is, we can get closer to user-defined control structures.)
Stretch goal
We can also allow extension methods on references:
That reads better than having to pass the reference being updated as a parameter.
Cons
The risk, as always, is that it becomes harder to read code.
If it's considered too "magical" that
notFirst(first)
can change the value offirst
, even though it looks like a normal function argument, we could also add an operator at the call-site, so it would benotFirst(&first)
. Then it's clear at the call-point too that something is going one. That doesn't work well for extensions on references. It can work, but&list[x].update(...)
does not have a clear delimiter for which expression is the reference. Is it&list
or&list[x]
? Might need syntax like&(list[x])
, or evenref(list[x])
which makes it more verbose. An alternative would be using&.foo()
to call the extension method, but that doesn't work for operators. All in all, syntax is tricky for reference extensions.If references are implicitly wrapped getter/setter functions, then they are objects, assignable to
Object
, but with no clear way to convert back. Would we allowas int&
as a type cast? (Probably necessary, if assigning toObject
is allowed, and not making that allowed is a significant change to the type system).Are references invariant? Should they be? (I guess final references are covariant, mutable references are invariant, and if we go for
in
/out
/inout
thenout
references are contravariant). In either case, that would require variance (#524) to exist in the language.The text was updated successfully, but these errors were encountered: