Open lrhn opened 3 years ago
ref
, similar to vala.
My two cents: Make the &
required at the call site as well. It makes using a function that changed to pass-by-reference easier to spot and callers have to think about any consequences (which helps readability).
We would probably want both ref
and out
parameters, like in C#. Otherwise, the definite assignment analysis doesn't work as well because it can't tell which parameters will be definitely assigned by being passed by reference and which must be assigned before being passed as arguments.
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 ... swap
? :grin:
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. Maybe even templates like #1353, where you want to abstract over a variable (although, in that case you probably want to abstract over the name and introduce new members with derived names).
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 +=
in term of the reference of its first operand, without having to re-specify it for every kind of variable and index operation. Basically, anywhere the spec requires an assignable expression.)
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.
the macro receives its parameters basically by name.
Macros receive the parameter expression as unevaluated syntax. You could write a swap()
macro that does what you want, but the macro author would have to be careful around variable capture and hygiene. (Also, we don't currently have any plan for expression level macros.)
at the very least there should be an hint on the call site.
Hence the recommendation to use &
at the call-site, like notFirst(&first)
That looks like
class Ref<T> {
T? value;
Ref([this.value]);
}
with more steps.
If you want add a hasValue
, you need to do more work, but what you show here is functionally equivalent to a nullable public variable. Change <T>
to <T extends Object>
, and you can use null
to represent no value.
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 null
from no value, like the list based one, just with a simpler wrapper.
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 sayin 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 forin
/out
/inout
thenout
references are contravariant). In either case, that would require variance (#524) to exist in the language.