Open eernstg opened 11 months ago
and not many iterables have element type
void
I use it sometimes. There are times when I don't care for the type of the element. Instead of using Iterable<Object?>
I use Iterable<void>
to convey the semantic that the element type is irrelevant.
Interesting! Do you really never need to access those objects? Then why do you iterate over them? ;-)
(If you do need to access them in any way then I'd expect the type void
to create some difficulties.)
Then why do you iterate over them?
Well, I don't. In these cases, I usually want the concept of having a list (reference) or to call one of its fields i.e. lenght
or isEmpty
. Surely, I could do it differently instead of using Iterable<void>
, but it's just the way I do.
If wouldn't mind changing the way I program for a greater good, tho.
Switch expression cases are another place we allow this today:
var y = switch(x) {
_ => print('hello'),
};
Switch expressions is a good example: I did not mention the conditional expression b ? e1 : e2
because I think it's benign: If e1
or e2
has type void
then the expression has type void
, and then we'll constrain the usage of the conditional expression. So we accept that void
is propagated one step because we will be there to catch it at the next step. Switch is similar.
void main() {
// Currently: `y` gets inferred type `void`, no error.
// New rule: Error, using "void value" to initialize variable.
var y = switch(x) {
_ => print('hello'),
};
// Currently: OK.
// New rule: OK: Discard the result of a void expression (after one step, but that's OK).
(switch(x) {
_ => print('hello'),
});
}
@mateusfccp wrote:
I usually want the concept of having a list (reference) or to call one of its fields
OK! Then I misunderstood you. I was just focusing on the for
statement, not on the iterable per se. There is no problem in having an Iterable<T>
typed statically as an Iterable<void>
in the case where you actually don't want to access the elements, I agree that this is actually a quite natural way to express the intent.
But then there's no problem because this issue doesn't say anything about Iterable<void>
being a bad type, it just says that "we shouldn't propagate the value of an expression of type void
", and that's actually the same idea as "we shouldn't look at the elements of this list".
We could, for example, specify that an
=>
function with return typevoid
returns null, no matter what,
This would be another implicit conversion, essentially. Those seem to carry a lot of weight in terms of implementation complexity for relatively little value. I'd be loath to add another unless we felt it was particularly compelling.
I'm also disinclined to have code using a concrete type behave differently from the same code where the type is replaced with a type parameter and then instantiated with that same type:
void concrete(void param) => param;
T generic<T>(T param) => param;
main() {
print(concrete('hi') as Object);
print(generic<void>('hi') as Object);
}
Currently this (admittedly weird) program prints "hi" twice. I think it would be a little weird if it instead printed null
then "hi".
and we could eliminate the permission to use the value of a
void
expression to initialize/assign a new value to a variable,
Sure, I could see us abolishing this. But it seems relatively low value to do so. We almost certainly have more important things we could be doing. :)
or to return such a value.
I admit that maybe this my old C programming history showing, but I do sometimes indulge in the luxury of being able to do:
void someVoidFunction() { ... }
void someOtherVoidFunction(bool condition) {
if (condition) return someVoidFunction();
// ...
}
Instead of:
void someVoidFunction() { ... }
void someOtherVoidFunction(bool condition) {
if (condition) {
someVoidFunction();
return;
}
// ...
}
There is some interest in making return
and some other control flow statements into expressions so that they can be used in switch expression cases (#2025). If we were to do that, then it becomes more compelling to me to allow void
-typed return values in return
expressions since switch expression cases can currently only have a single expression. So with the above example, you would be able to write it as:
void someOtherVoidFunction(int x) {
var y = switch (x) {
0 => return someVoidFunction(),
_ => x + 1,
};
// ...
}
But there is no way to use a switch expression and expand that out to:
void someOtherVoidFunction(int x) {
var y = switch (x) {
0 => { // <-- Error. Can't have a block here.
someVoidFunction();
return;
},
_ => x + 1,
};
// ...
}
(Of course, allowing blocks in switch expression cases would solve this (#3117). But that's a much bigger issue. In the meantime, allowing void expressions here seems to offer some use.)
This would be another implicit conversion, essentially.
Not necessarily.
It would be a static compilation rule that applies to any function with a static return type of void
and an => e
body, effectively compiling it as a body of {e;}
.
There is no runtime coercion of the value, it's just not returned. The void ... => ...
combination is considered syntactic sugar for the non-=>
body.
Which is only needed because the formatter refuses to allow
void foo(x) { e; }
on one line, even if it would fit. Using void ... => ...
is entirely about saving lines, it has no other reason to be allowed.
I'd personally also disallow e as T
when e
has type void
.
And I find return someVoidFunction();
confusing, and never accept it silently in a code review.
Allowing a return-void in a switch expression isn't reason enough, it's just a sign that you want n statements in a switch expression case, and in that particular case, n == 1 statement happened to be enough. Still won't work for n > 1, so we should fix the general problem, not allow a specific confusing code as a hacky partial solution.
My usual recommendation is that every expression with a static context type of void
throws away it's value and evaluates to null
instead. That is a coercion, just a particularly simple one. And an expression with static type void
can only occur where the value is never inspected or assigned up anything, with no exceptions for as
casts or dynamic
/void
context types.
No way out of a statically known void
.
This would be another implicit conversion, essentially.
Not necessarily. It would be a static compilation rule that applies to any function with a static return type of
void
and an=> e
body, effectively compiling it as a body of{e;}
.There is no runtime coercion of the value, it's just not returned. The
void ... => ...
combination is considered syntactic sugar for the non-=>
body.
Sure, it would be like int-to-double, which is another implicit conversion. :)
Which is only needed because the formatter refuses to allow
void foo(x) { e; }
on one line, even if it would fit. Using
void ... => ...
is entirely about saving lines, it has no other reason to be allowed.
I feel attacked. :) I have considered allowing { e; }
, but the problem is that users don't want that in many cases for single-line functions so figuring out which should and shouldn't be collapsed is hard.
it's just a sign that you want n statements in a switch expression case, and in that particular case, n == 1 statement happened to be enough.
I'd be happy to eliminate statements from the language! :D
Sure, it would be like int-to-double, which is another implicit conversion. :)
Now you're just messing with me! :stuck_out_tongue_closed_eyes:
(He knows I keep insisting that int-literal-evaluates-to-double-value is not a conversion or coercion, it's just that integer numerals have a context-type dependent semantics. That is, the same syntax have different semantics depending on the context type. But so does the literal [1]
, which can mean any of <int>[1]
, <int?>[1]
, <FutureOr<int>>[1]
, <num>[1]
, etc., depending on the context type. Context-type dependent semantics is what downwards type inference is.)
the problem is that users don't want that in many cases for single-line functions so figuring out which should and shouldn't be collapsed is hard.
And there is no good syntax to opt you in or out. Which is probably why we have => e;
as opt-in to a single-line void
function, even if it's somewhat misleading.
(But as an opinionated formatter, I'd just pick the lower-line-count version every time, and users will just have to be happy about it!)
The type
void
in Dart works as a compile-time only marker indicating that the given value has no meaningful interpretation, and it should simply be discarded.This works nicely for expressions of type
void
, e.g.,print('Hello!')
, which are accepted with no issues when used as an expression statement, but flagged as an error (not just a warning) if we try to use the value:We allow an expression of type
void
to occur in certain locations which are enumerated in an explicit allowlist.However, several of the allowed positions are special in that they propagate the value which was obtained by evaluation of an expression of type
void
, based on the rule, roughly, that the target has typevoid
as well.Some of these situations occur very rarely (for example, not many variables have declared type
void
, and not many iterables have element typevoid
), but others come up repeatedly (=>
functions are widely used, andvoid
is a rather common return type).This issue is intended to push us in the direction of having fewer situations where such values are propagated.
We could, for example, specify that an
=>
function with return typevoid
returns null, no matter what, and we could eliminate the permission to use the value of avoid
expression to initialize/assign a new value to a variable, or to return such a value.We will still have loopholes, of course. For example,
void
can be the value of a type parameter:A proposal along these lines would be a breaking change (for instance,
return e;
might need to be changed toe; return;
), but I still think that it will be useful to have an issue where this topic is discussed, and we might just do some of these things at some point. For example, I haven't heard any complaints aboutawait print('Hello!')
being an error since Dart 2.12. ;-)