Just piping up in a sort of pre-discussion way to see if there might be
any interest in making "return" an expression - in the same way and for
many (though not all) of the same reasons that "throw" was made an
expression in 8.0.
Both return and throw "end the current execution"; the difference is
that "throw" does it because things went bad, while "return" does it
because things went well and nothing more needs doing.
So, for example:
$result = query('foo') ?? return false;
rather than
if(($result = query('foo')) === null) return false;
or
$split = match($len) {
0 => throw new UnderflowException("Unexpectedly empty"),
1 => return false,
2,3,5,7 => return true,
4,6,8,9 => $this->smol($len),
default => $this->foo($len, $scale)
};
instead of
if($len === 0)
throw new UnderflowException("Unexpectedly empty");
if($len === 1)
return false;
if($len === 2 || $len === 3 || $len === 5 || $len === 7)
return true;
if($len === 4 || $len === 6 || $len === 8 || $len === 9)
$split = $this->smol($len);
else
$split = $this->foo($len, $scale);
.
A return expression works entirely by its side-effects (passing control
and the value of its operand back to the calling scope); its own type is
"never".
The weirdest behaviour I can see at this early stage is the main way it
deviates from "reasons to have throw as an expression":
fn($x) => return $x * 2
would work, but not in the way you'd think it does; it expands out to
function($x) {
return return $x * 2;
}
Which (like "throw throw") is syntactically legal, but the redundant
operator is redundant. Semantically, however, it could be problematic:
The anonymous function's return type is technically "never", since that
is what the type of its return statement's operand is. But that return
statement never(!) completes, because said operand causes the function
to end prematurely, causing the numeric value to be returned instead.
And, of course, a numeric value is not a "never".
What this will do to type declarations I don't know. But checking that
the body of an arrow function is a return expression and at least
notifying the author that it's redundant could be done.
This I guess would also impact ordinary return statements: they are now
expressions that evaluate to "never", and that could have consequences
for how return types and return type declarations are determined in
general...
If necessary, I suppose, the type of a return expression could be that
of its operand - it's just that no-one will ever see it because
execution is always abandoned before then.
Just piping up in a sort of pre-discussion way to see if there might be
any interest in making "return" an expression - in the same way and for
many (though not all) of the same reasons that "throw" was made an
expression in 8.0.Both return and throw "end the current execution"; the difference is
that "throw" does it because things went bad, while "return" does it
because things went well and nothing more needs doing.So, for example:
$result = query('foo') ?? return false;
rather than
if(($result = query('foo')) === null) return false;
or
$split = match($len) {
0 => throw new UnderflowException("Unexpectedly empty"),
1 => return false,
2,3,5,7 => return true,
4,6,8,9 => $this->smol($len),
default => $this->foo($len, $scale)
};instead of
if($len === 0)
throw new UnderflowException("Unexpectedly empty");
if($len === 1)
return false;
if($len === 2 || $len === 3 || $len === 5 || $len === 7)
return true;
if($len === 4 || $len === 6 || $len === 8 || $len === 9)
$split = $this->smol($len);
else
$split = $this->foo($len, $scale);.
A return expression works entirely by its side-effects (passing control
and the value of its operand back to the calling scope); its own type is
"never".The weirdest behaviour I can see at this early stage is the main way it
deviates from "reasons to have throw as an expression":fn($x) => return $x * 2
would work, but not in the way you'd think it does; it expands out to
function($x) {
return return $x * 2;
}Which (like "throw throw") is syntactically legal, but the redundant
operator is redundant. Semantically, however, it could be problematic:
The anonymous function's return type is technically "never", since that
is what the type of its return statement's operand is. But that return
statement never(!) completes, because said operand causes the function
to end prematurely, causing the numeric value to be returned instead.
And, of course, a numeric value is not a "never".What this will do to type declarations I don't know. But checking that
the body of an arrow function is a return expression and at least
notifying the author that it's redundant could be done.This I guess would also impact ordinary return statements: they are now
expressions that evaluate to "never", and that could have consequences
for how return types and return type declarations are determined in
general...If necessary, I suppose, the type of a return expression could be that
of its operand - it's just that no-one will ever see it because
execution is always abandoned before then.
Hi Morgan,
From an observability point of view, you lost me when you said return's type is "never".
We can clearly see the type is "null", unless you mean something more abstract?
Are you thinking of return as a function call, so you could write it out like:
return(return($x*2))
I guess in that case, you could conceptually think of its arguments as function that returns what to return? Thus the above is just sugar for:
return function() {
return fn() => $x*2;
}
Which would just collapse down to:
return $x*2;
I think you could handwave it down to that. The removal of the infinite functions is just an optimisation.
— Rob
From an observability point of view, you lost me when you said return's
type is "never".https://3v4l.org/plpIf#v8.5.0 https://3v4l.org/plpIf#v8.5.0
We can clearly see the type is "null", unless you mean something more
abstract?
I'm referring to the expression's value as it may exist in a larger
expression, primarily for type checking. To use the match{} example,
what value does $split get when $len == 1, because in that case the
statement boils down to "$split = return false;"? Evaluation never comes
back to the assignment to assign $split anything, in the same way that a
call to a function of type never shouldn't see evaluation coming back to
the call site.
If $split were something still visible after function return ("$o->p"),
since the assignment didn't happen it would still have its previous value.
But as I alluded and you noted, this wouldn't be observable: the caller
gets the value of the return expression's operand (or null, if no such
operand was provided, in which case why are you trying to see the value
of a function of type void?)
If you were doing static analysis, making the type of a return
expression that of its operand could be an issue: in that match{}
example there is no assertion that $split is supposed to contain a
boolean. Let's say it's supposed to be an int. "int|bool" would be an
incorrect inference; since never is a bottom type and thus a subtype of
every type, "int|never" == "int".
On the other hand, "int|Exception" is just as invalid, but throwing
exceptions from inside expressions is pretty well-behaved; I think
modelling return's semantics the same way would work, but I don't know
enough of the internals to make that judgement.
Basically, the type of a return expression would be the type of a throw.
From an observability point of view, you lost me when you said return's
type is "never".https://3v4l.org/plpIf#v8.5.0 https://3v4l.org/plpIf#v8.5.0
We can clearly see the type is "null", unless you mean something more
abstract?I'm referring to the expression's value as it may exist in a larger
expression, primarily for type checking. To use the match{} example,
what value does $split get when $len == 1, because in that case the
statement boils down to "$split = return false;"? Evaluation never comes
back to the assignment to assign $split anything, in the same way that a
call to a function of type never shouldn't see evaluation coming back to
the call site.If $split were something still visible after function return ("$o->p"),
since the assignment didn't happen it would still have its previous value.But as I alluded and you noted, this wouldn't be observable: the caller
gets the value of the return expression's operand (or null, if no such
operand was provided, in which case why are you trying to see the value
of a function of type void?)If you were doing static analysis, making the type of a return
expression that of its operand could be an issue: in that match{}
example there is no assertion that $split is supposed to contain a
boolean. Let's say it's supposed to be an int. "int|bool" would be an
incorrect inference; since never is a bottom type and thus a subtype of
every type, "int|never" == "int".On the other hand, "int|Exception" is just as invalid, but throwing
exceptions from inside expressions is pretty well-behaved; I think
modelling return's semantics the same way would work, but I don't know
enough of the internals to make that judgement.Basically, the type of a return expression would be the type of a throw.
I would say the weirdest thing about making return an expression boils down to where it could then be used:
myFunc(return 123);
$arr = [1, 2, return 3];
1 + 2 + return 3
Are just some basic examples. As I mentioned in the other thread about this, it would make finding method/function returns like hunting for a needle in a haystack.
— Rob
From an observability point of view, you lost me when you said return's
type is "never".https://3v4l.org/plpIf#v8.5.0 https://3v4l.org/plpIf#v8.5.0
We can clearly see the type is "null", unless you mean something more
abstract?I'm referring to the expression's value as it may exist in a larger
expression, primarily for type checking. To use the match{} example,
what value does $split get when $len == 1, because in that case the
statement boils down to "$split = return false;"? Evaluation never comes
back to the assignment to assign $split anything, in the same way that a
call to a function of type never shouldn't see evaluation coming back to
the call site.If $split were something still visible after function return ("$o->p"),
since the assignment didn't happen it would still have its previous value.But as I alluded and you noted, this wouldn't be observable: the caller
gets the value of the return expression's operand (or null, if no such
operand was provided, in which case why are you trying to see the value
of a function of type void?)If you were doing static analysis, making the type of a return
expression that of its operand could be an issue: in that match{}
example there is no assertion that $split is supposed to contain a
boolean. Let's say it's supposed to be an int. "int|bool" would be an
incorrect inference; since never is a bottom type and thus a subtype of
every type, "int|never" == "int".On the other hand, "int|Exception" is just as invalid, but throwing
exceptions from inside expressions is pretty well-behaved; I think
modelling return's semantics the same way would work, but I don't know
enough of the internals to make that judgement.Basically, the type of a return expression would be the type of a throw.
I would say the weirdest thing about making return an expression boils
down to where it could then be used:myFunc(return 123);
$arr = [1, 2, return 3];
1 + 2 + return 3
Are just some basic examples. As I mentioned in the other thread about
this, it would make finding method/function returns like hunting for a
needle in a haystack.— Rob
I think all those examples actually make sense. return <expr> would
itself become an expression, which means you could use it anywhere, all
those places already support throw.
example: https://3v4l.org/nqbkU
static analyzers would be able to warn about unreachable code and never
being used within expressions:
https://mago.carthage.software/playground#019c1d99-449c-e1bf-2517-726d0b9f9b5f
Just piping up in a sort of pre-discussion way to see if there might be
any interest in making "return" an expression - in the same way and for
many (though not all) of the same reasons that "throw" was made an
expression in 8.0.Both return and throw "end the current execution"; the difference is
that "throw" does it because things went bad, while "return" does it
because things went well and nothing more needs doing.So, for example:
$result = query('foo') ?? return false;
rather than
if(($result = query('foo')) === null) return false;
or
$split = match($len) {
0 => throw new UnderflowException("Unexpectedly empty"),
1 => return false,
2,3,5,7 => return true,
4,6,8,9 => $this->smol($len),
default => $this->foo($len, $scale)
};instead of
if($len === 0)
throw new UnderflowException("Unexpectedly empty");
if($len === 1)
return false;
if($len === 2 || $len === 3 || $len === 5 || $len === 7)
return true;
if($len === 4 || $len === 6 || $len === 8 || $len === 9)
$split = $this->smol($len);
else
$split = $this->foo($len, $scale);.
A return expression works entirely by its side-effects (passing control
and the value of its operand back to the calling scope); its own type is
"never".The weirdest behaviour I can see at this early stage is the main way it
deviates from "reasons to have throw as an expression":fn($x) => return $x * 2
would work, but not in the way you'd think it does; it expands out to
function($x) {
return return $x * 2;
}Which (like "throw throw") is syntactically legal, but the redundant
operator is redundant. Semantically, however, it could be problematic:
The anonymous function's return type is technically "never", since that
is what the type of its return statement's operand is. But that return
statement never(!) completes, because said operand causes the function
to end prematurely, causing the numeric value to be returned instead.
And, of course, a numeric value is not a "never".What this will do to type declarations I don't know. But checking that
the body of an arrow function is a return expression and at least
notifying the author that it's redundant could be done.This I guess would also impact ordinary return statements: they are now
expressions that evaluate to "never", and that could have consequences
for how return types and return type declarations are determined in
general...If necessary, I suppose, the type of a return expression could be that
of its operand - it's just that no-one will ever see it because
execution is always abandoned before then.Hi Morgan,
From an observability point of view, you lost me when you said return's type is "never".
We can clearly see the type is "null", unless you mean something more abstract?
Are you thinking of return as a function call, so you could write it out like:
return(return($x*2))
I guess in that case, you could conceptually think of its arguments as function that returns what to return? Thus the above is just sugar for:
return function() {
return fn() => $x*2;
}Which would just collapse down to:
return $x*2;
I think you could handwave it down to that. The removal of the infinite functions is just an optimisation.
— Rob
Hi Rob,
I think never as a result of return makes complete sense. never
indicates something does not result in a value, e.g throw or exit
where the control flow is given away, return as an expression would
do the same thing, it will move a value out of this function scope to
the caller, resulting in never within the function. ( the same would
apply if break and continue are to be made expressions in the
future ).
As for return as a function call, i don't think that is needed, once
return <expr> is supported as an expression, you would be able to
return return("foo"); as that is just return with the RHS being a
parenthesized expression; the reason i think return does not make a
sense as a "function" is because i think function call rules should
not apply to it, e.g what happens if you provide multiple arguments?
Does it throw an ArgumentCountError? This is avoidable by making
return ("a", "b") a parse error.
Cheers.
Seifeddine.
Hi Morgan
So, for example:
$result = query('foo') ?? return false;rather than
if(($result = query('foo')) === null) return false;
Just a heads-up. This has been suggested before and leads to
non-trivial issues with regards to mid-expression live-variable
cleanup. I encountered similar issues for block expressions, for which
{ return; } is effectively equivalent to return. I explained the
problem in more detail here:
https://wiki.php.net/rfc/match_blocks#technical_implications_of_control_statements
Ilija