[PHP-DEV] [RFC] Transform void into an alias for null

Hello internals,

This is the second RFC out of a set of type system related RFCs I want to propose for PHP 8.5.

The objective is to fix a weird quirk of PHP's type system, where void lives in its own type hierarchy.
This is visible mainly in that a lack of return type is not isomorphic to a function that has a return type of mixed.

Let me know what you think about it.

RFC: PHP: rfc:void-as-null

Best regards,

Gina P. Banyard

On Mon, Jun 2, 2025, at 11:27 AM, Gina P. Banyard wrote:

Hello internals,

This is the second RFC out of a set of type system related RFCs I want
to propose for PHP 8.5.

The objective is to fix a weird quirk of PHP's type system, where void
lives in its own type hierarchy.
This is visible mainly in that a lack of return type is not isomorphic
to a function that has a return type of mixed.

Let me know what you think about it.

RFC: PHP: rfc:void-as-null

Best regards,

Gina P. Banyard

The result of this RFC is that the following would no longer be an error, yes?

function test(): void {
  print "test";
}

// This currently gives an error, but you propose that it
// would change to set $val to null?
$val = test();

--Larry Garfield

The RFC mentions that this will now become valid:

function foo(): void {
    return null;
}

But what about the opposite:

function foo(): null {
    return;
}

or what Larry was trying to suggest:

function foo(): null {
    print 'test';
}

$val = foo();

On 02/06/2025 17:27, Gina P. Banyard wrote:

The objective is to fix a weird quirk of PHP's type system, where void lives in its own type hierarchy.
This is visible mainly in that a lack of return type is not isomorphic to a function that has a return type of mixed.

I think if "void" was added now, it would be an attribute, rather than a type. It is in effect the exact opposite of #[\NoDiscard], and distinguishes between these two cases:

interface Foo {
public function getSomething(): ?Something;
}

class MyFoo implements Foo {
public function getSomething(): null {
// My result is always null, but still meaningful to consumers of the Foo interface
return null;
}

\#\[\\DiscardReturn\]
public function doSomething\(\): null \{
     // I have no meaningful information to return; any assignment of my implicit value is a mistake
\}

}

I agree the type hierarchy you describe is weird, but rather than throwing away the functionality completely, I wonder if we can make it more consistent:

- Make "no return type declared" and "mixed" equivalent
- Make "void" a sub-type of "null", and therefore a sub-type of "mixed"

If I've got that right, this would then be legal:

class A{ public function foo() {} } class B extends A{ public function foo(): mixed{} }
class C extends B{ public function foo(): null{} }
class D extends C{ public function foo(): void{} }
class E extends D{ public function foo(): never {} }

That seems reasonable enough; I may have missed something important, though.

Regards,

--
Rowan Tommins
[IMSoP]

Hi Gina

On Mon, Jun 2, 2025 at 6:28 PM Gina P. Banyard <internals@gpb.moe> wrote:

RFC: PHP: rfc:void-as-null

After a read, I think I fundamentally disagree with the proposal. It
says (regarding the status-quo):

* void is not a subtype of a function with a mixed return type

This is laid out as a downside, but I don't think it is. Consider this
example under the specified behavior:

    interface MapInterface {
        public function set(string $key, mixed $value): void;
    }

    class Map implements MapInterface {
        public function set(string $key, mixed $value): void {
            // Store the key/value pair _somehow_
        }
    }

Let's assume the return type `MapInterface::set()` `mixed` instead,
where the intention is for `set()` to return the previous value, or
`null` if there was none. This change will go completely unnoticed by
`Map::set()`, because `void` is now just `null`, which is a subtype of
`mixed`. This is a bug that would previously have been caught,
notifying you that you're supposed to return _something_.

Similarly, the code `function foo(): void { return null; }` is now
proposed to be valid, and I assume the inverse for `void`/`return;` is
also true. In this example, we now update `Map::set()` to the new
return type.

    class Map implements MapInterface {
        public function set(string $key, mixed $value): mixed {
            $oldValue = /* Fetch old value _somehow_ */;
            // Store the key/value pair _somehow_

            if (!$this->observers) {
                return; // This line was here before.
            }

            $this->observers->notify($key, $oldValue, $value);

            return $oldValue;
        }
    }

Good examples are hard, but imagine `Map::set()` would allow notifying
a list of observers about changes to the map. Previously, the
`return;` would have prevented an erroneous call to `notify()` on
`null`. However, now it is missing the returning of `$oldValue`. This
is another bug that would previously have been caught. In fact, even a
missing trailing `return $something;` would not be caught anymore.

IMO, these checks are useful enough not to be removed.

Please let me know if I'm missing anything.

Ilija

On 02/06/2025 23:50, Rowan Tommins [IMSoP] wrote:

I agree the type hierarchy you describe is weird, but rather than throwing away the functionality completely, I wonder if we can make it more consistent:

- Make "no return type declared" and "mixed" equivalent
- Make "void" a sub-type of "null", and therefore a sub-type of "mixed"

I think that null and void are semantically very different so I'd like to suggest just making void subtype of mixed. This will both keep the semantic meaning of void and make mixed and undeclared mean the same thing.

--
Anton

On 02/06/2025 20:01, Larry Garfield wrote:

The result of this RFC is that the following would no longer be an error, yes?

function test(): void {
   print "test";
}

// This currently gives an error, but you propose that it
// would change to set $val to null?
$val = test();

There is no error: Online PHP editor | output for UD4vn

--
Anton

p.s. sorry Larry, first time I forgot that I'm answering to the mailing list :smiley:

On 03.06.2025 at 03:36, Anton Smirnov wrote:

On 02/06/2025 20:01, Larry Garfield wrote:

The result of this RFC is that the following would no longer be an
error, yes?

function test(): void {
print "test";
}

// This currently gives an error, but you propose that it
// would change to set $val to null?
$val = test();

There is no error: Online PHP editor | output for UD4vn

I guess that Larry meant `return` instead of `print`:
<https://3v4l.org/7dtYH&gt;\.

On Monday, 2 June 2025 at 18:08, Kamil Tekiela <tekiela246@gmail.com> wrote:

The RFC mentions that this will now become valid:

function foo(): void {
return null;
}

But what about the opposite:

function foo(): null {
return;
}

This would also work indeed.

or what Larry was trying to suggest:

function foo(): null {
print 'test';
}

$val = foo();

This would also work, as null and void would be isomorphic.
However, if you have a return type of T|null then you'd get errors in both cases.

I will clarify this in the RFC.

Best regards,

Gina P. Banyard

On Monday, 2 June 2025 at 21:53, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

On 02/06/2025 17:27, Gina P. Banyard wrote:

The objective is to fix a weird quirk of PHP's type system, where void lives in its own type hierarchy.
This is visible mainly in that a lack of return type is not isomorphic to a function that has a return type of mixed.

I think if "void" was added now, it would be an attribute, rather than a type. It is in effect the exact opposite of #[\NoDiscard], and distinguishes between these two cases:

interface Foo {
public function getSomething(): ?Something;
}

class MyFoo implements Foo {
public function getSomething(): null {
// My result is always null, but still meaningful to consumers of the Foo interface
return null;
}

#[\DiscardReturn]
public function doSomething(): null {
// I have no meaningful information to return; any assignment of my implicit value is a mistake
}

}

A function that always returns the same value is not meaningful to a consumer, the only exception is in a class hierarchy with an overloaded method.
But in that case the consumer expects potential other concrete values, so this point is a bit moot IMHO.
In the same way as saving the result of print() is pointless because it always returns 1, or a function that always returns true.

I agree the type hierarchy you describe is weird, but rather than throwing away the functionality completely, I wonder if we can make it more consistent:

- Make "no return type declared" and "mixed" equivalent
- Make "void" a sub-type of "null", and therefore a sub-type of "mixed"

If I've got that right, this would then be legal:

class

A

{

public

function

foo

(

)

{

}

}

class

B extends A

{

public

function

foo

(

)

: mixed

{

}

}

class

C extends B

{

public

function

foo

(

)

: null

{

}

}

class

D extends C

{

public

function

foo

(

)

: void

{

}

}

class

E extends D

{

public

function

foo

(

): never

{

}

}

That seems reasonable enough; I may have missed something important, though

Not sure if it is important, but you are missing the case where null takes part in a union type.
Your proposed change would also allow the following class hierarchy:

class

A

{

public

function

foo

(

)

{

}

}

class

B extends A

{

public

function

foo

(

)

: mixed

{

}

}

class

C extends B

{

public

function

foo

(

)

: string|int|null

{

}

}

class

D extends C

{

public

function

foo

(

)

: void

{

}

}

class

E extends D

{

public

function

foo

(

): never

{

}

}

But if people think this type hierarchy makes more sense, then sure.
I am not convinced, as I think it is a *good* thing PHP always returns a value from a function.
Lying about this just seems pointless and leading to misunderstandings about how the language behaves.
Best regards,
Gina P. Banyard

Hi

Am 2025-06-03 01:46, schrieb Ilija Tovilo:

IMO, these checks are useful enough not to be removed.

I agree with Ilija (and also Rowan). To me there is an important semantic difference between “not returning anything” and “always returning null”. I believe that `void` being in a distinct type hierarchy is the right choice and when considering “untyped returns” to be soft-deprecated / discouraged, there are no inconsistencies either.

Best regards
Tim Düsterhus

On Tuesday, 3 June 2025 at 02:36, Anton Smirnov <sandfox@sandfox.me> wrote:

On 02/06/2025 23:50, Rowan Tommins [IMSoP] wrote:

> I agree the type hierarchy you describe is weird, but rather than
> throwing away the functionality completely, I wonder if we can make it
> more consistent:
>
> - Make "no return type declared" and "mixed" equivalent
> - Make "void" a sub-type of "null", and therefore a sub-type of "mixed"

I think that null and void are semantically very different so I'd like
to suggest just making void subtype of mixed. This will both keep the
semantic meaning of void and make mixed and undeclared mean the same thing.

You are going to need to expand on why you think those two are semantically very different.
PHP does not have the concept of "execution control is returned to the calling scope, but no concrete value is returned".
And this is a good thing IMHO, as it means you can always take the result of a function.

Moreover, "just making void subtype of mixed" can mean everything and nothing.
Do you mean for void to live on its own island like the int or string types?
Make it a subtype of null like Roman suggested?
Have void be a super type of some weird union of types representing scalars int|string|float|bool?
Something else altogether?

Best regards,

Gina P. Banyard

On Tuesday, 3 June 2025 at 00:48, Ilija Tovilo <tovilo.ilija@gmail.com> wrote:

Hi Gina

On Mon, Jun 2, 2025 at 6:28 PM Gina P. Banyard internals@gpb.moe wrote:

> RFC: PHP: rfc:void-as-null

After a read, I think I fundamentally disagree with the proposal. It
says (regarding the status-quo):

> * void is not a subtype of a function with a mixed return type

This is laid out as a downside, but I don't think it is. Consider this
example under the specified behavior:

interface MapInterface {
public function set(string $key, mixed $value): void;
}

class Map implements MapInterface {
public function set(string $key, mixed $value): void {
// Store the key/value pair somehow
}
}

Let's assume the return type `MapInterface::set()` `mixed` instead,
where the intention is for `set()` to return the previous value, or
`null` if there was none. This change will go completely unnoticed by
`Map::set()`, because `void` is now just `null`, which is a subtype of
`mixed`. This is a bug that would previously have been caught,
notifying you that you're supposed to return something.

Similarly, the code `function foo(): void { return null; }` is now
proposed to be valid, and I assume the inverse for `void`/`return;` is
also true. In this example, we now update `Map::set()` to the new
return type.

class Map implements MapInterface {
public function set(string $key, mixed $value): mixed {
$oldValue = /* Fetch old value somehow */;
// Store the key/value pair somehow

if (!$this->observers) {

return; // This line was here before.
}

$this->observers->notify($key, $oldValue, $value);

return $oldValue;
}
}

Good examples are hard, but imagine `Map::set()` would allow notifying
a list of observers about changes to the map. Previously, the
`return;` would have prevented an erroneous call to `notify()` on
`null`. However, now it is missing the returning of `$oldValue`. This
is another bug that would previously have been caught. In fact, even a
missing trailing `return $something;` would not be caught anymore.

IMO, these checks are useful enough not to be removed.

Please let me know if I'm missing anything.

I must say the examples are not really convincing to me. :slight_smile:
The compile time warning would *only* be suppressed for case where the return type of the function/method is *exactly* null or void,
the moment you have a union type or mixed you would still get the compile time error.
I realise that I didn't properly describe the behaviour of my PoC implementation in the RFC, apologies for that.

And a type system on its own is never going to be able to catch all implementation bugs,
even with dependant and effect types, you can write code that passes a type checker yet not do what is expected.
A type system should be logical, and the fact that the top type (mixed) is not a super-type of all types doesn't make any sense.
We *specifically* included null within mixed as having a proper top type is required, even if many people would have preferred it to exclude null and just use ?mixed if you wanted "everything".

As said to Anton, PHP does not have the concept/capability of "not returning a value" (unless you throw/exit)

Final small cheeky note, we don't warn/error in other cases where we detect dead code, we let the optimizer get rid of them, is this something we should warn about?

Best regards,

Gina P. Banyard

On Tuesday, 3 June 2025 at 16:18, Tim Düsterhus <tim@bastelstu.be> wrote:

Am 2025-06-03 01:46, schrieb Ilija Tovilo:

> IMO, these checks are useful enough not to be removed.

I agree with Ilija (and also Rowan).

Well, I'm not sure Ilija and Rowan agree between each other.
AFAIU Ilija thinks void living on its weird island is good.
Meanwhile, Rowan thinks it is somewhat confusing and that void should be a subtype of null.

To me there is an important semantic difference between “not returning anything”
and “always returning null”.
I believe that `void` being in a distinct type
hierarchy is the right choice and when considering “untyped returns” to
be soft-deprecated / discouraged, there are no inconsistencies either.

Then our type system is not logical as we have a top type that is not actually a top type.
This is something that will cause problems for function types, especially if they have generic arguments.
(if not causing weird issues on its own just for generics)

Best regards,

Gina P. Banyard

On Tue, Jun 3, 2025, at 4:33 AM, Christoph M. Becker wrote:

On 03.06.2025 at 03:36, Anton Smirnov wrote:

On 02/06/2025 20:01, Larry Garfield wrote:

The result of this RFC is that the following would no longer be an
error, yes?

function test(): void {
print "test";
}

// This currently gives an error, but you propose that it
// would change to set $val to null?
$val = test();

There is no error: Online PHP editor | output for UD4vn

I guess that Larry meant `return` instead of `print`:
<https://3v4l.org/7dtYH&gt;\.

No, I did not. I was sure I've run into places before where even trying to assign the return value of a void function to something gives me an error, but perhaps it was one of the many SA tools I use (IDE, PHPStan, etc.).

--Larry Garfield

On 03/06/2025 18:20, Gina P. Banyard wrote:

On Tuesday, 3 June 2025 at 02:36, Anton Smirnov <sandfox@sandfox.me> wrote:

On 02/06/2025 23:50, Rowan Tommins [IMSoP] wrote:

I agree the type hierarchy you describe is weird, but rather than
throwing away the functionality completely, I wonder if we can make it
more consistent:

- Make "no return type declared" and "mixed" equivalent
- Make "void" a sub-type of "null", and therefore a sub-type of "mixed"

I think that null and void are semantically very different so I'd like
to suggest just making void subtype of mixed. This will both keep the
semantic meaning of void and make mixed and undeclared mean the same thing.

You are going to need to expand on why you think those two are semantically very different.
PHP does not have the concept of "execution control is returned to the calling scope, but no concrete value is returned".
And this is a good thing IMHO, as it means you can always take the result of a function.

Basically I agree with Ilija and Tim that functions that "return null" and functions that "return nothing but it's shown as null for historical and compatibility reasons" are different, but I see some value in aligning mixed and undeclared return type behavior.

Moreover, "just making void subtype of mixed" can mean everything and nothing.
Do you mean for void to live on its own island like the int or string types?
Make it a subtype of null like Roman suggested?
Have void be a super type of some weird union of types representing scalars int|string|float|bool?
Something else altogether?

this:

> Do you mean for void to live on its own island like the int or string types?

never <- void <- mixed

--
Anton

Hi

Am 2025-06-03 17:42, schrieb Gina P. Banyard:

A type system should be logical, and the fact that the top type (mixed) is not a super-type of all types doesn't make any sense.

I do not consider `void` to be a type per se, but rather as an indicator for the absence of a value. Basically the difference between a “procedure” and a “function” [1]. That's why it makes sense to me to treat `void` differently from the other types. See also: The `(void)` cast which we decided to make a statement rather than an expression that always evaluates to `null`.

Best regards
Tim Düsterhus

[1] https://stackoverflow.com/a/721132

Hi

Am 2025-06-03 17:46, schrieb Gina P. Banyard:

> IMO, these checks are useful enough not to be removed.

I agree with Ilija (and also Rowan).

Well, I'm not sure Ilija and Rowan agree between each other.

I believe they agree in that the distinction between `void` and `null` is a useful one.

AFAIU Ilija thinks void living on its weird island is good.
Meanwhile, Rowan thinks it is somewhat confusing and that void should be a subtype of null.

But indeed I'm more aligned with Ilija than Rowan (that’s why Rowan’s name is in parentheses).

To me there is an important semantic difference between “not returning anything”
and “always returning null”.
I believe that `void` being in a distinct type
hierarchy is the right choice and when considering “untyped returns” to
be soft-deprecated / discouraged, there are no inconsistencies either.

Then our type system is not logical as we have a top type that is not actually a top type.

See my reply to your reply to Ilija.

Best regards
Tim Düsterhus

On Wednesday, 4 June 2025 at 09:50, Tim Düsterhus <tim@bastelstu.be> wrote:

Am 2025-06-03 17:42, schrieb Gina P. Banyard:

> A type system should be logical, and the fact that the top type (mixed)
> is not a super-type of all types doesn't make any sense.

I do not consider `void` to be a type per se, but rather as an indicator
for the absence of a value.

Well we fundamentally disagree on this topic then,
the RFC that introduced the void type [G1] used as justification for the name void:

The main reason to choose void over null is that it is the customary name to use for such a return type.
[...]
others [PLs] (TypeScript, ActionScript, Swift) do allow void functions in expressions, just as PHP does, by making them *implicitly return some unit type*.

*Emphasis mine*

This reads to me that the authors know that `void` would mean "return unit type", rather than "lack of return value".
Moreover, another justification is:

There's no precedent for it and the name doesn't seem to have been an *issue until now*.

*Emphasis mine*

Which, IMHO, is not the case any more in that there is an issue now with a split and nonsensical type hierarchy.

Basically the difference between a “procedure” and a “function” [1].
That's why it makes sense to me to treat `void` differently from the other types.

PHP does not, like many modern PLs, make a distinction between a "procedure" and a "function", as they are the same thing.
In many PLs a function that returns "nothing" means returning the unit type.
And PHP's unit type is null.

Making void and null isomorphic does not prevent people from using either type name to communicate intent in their source code that this function is "procedure" or a "true function".
And if the point of void is for a function to say that it causes side effects, then adding effect type declarations to PHP would be a better solution, e.g.

function foo(): null!my_side_effect1|my_side_effect2 {
   bar(); /* causes my_side_effect1 */
   foobar(); /* causes my_side_effect2 */
}

See also: The `(void)`
cast which we decided to make a statement rather than an expression that
always evaluates to `null`.

I do not see how this invalidates my argument nor support yours.

Best regards,

Gina P. Banyard

[1] https://stackoverflow.com/a/721132

[G1] PHP: rfc:void_return_type

On 2.6.2025 18:27:51, Gina P. Banyard wrote:

Hello internals,

This is the second RFC out of a set of type system related RFCs I want to propose for PHP 8.5.

The objective is to fix a weird quirk of PHP's type system, where void lives in its own type hierarchy.
This is visible mainly in that a lack of return type is not isomorphic to a function that has a return type of mixed.

Let me know what you think about it.

RFC: PHP: rfc:void-as-null

Best regards,

Gina P. Banyard

I have to agree with other posters here that the distinction between null and void is an useful one.

In particular I'd consider the null returned by void to be incidental rather than intentional. I consider the return value of void functions "some arbitrary value". It just happens to be null.
Like every function has to return something. But returning null is not an intrinsic property of a void function. It's an extrinsic one. You observe void functions to generally return null. But that null in itself is meaningless.

So, my counter-proposal would be allowing covariance with void and allowing everything, including non-nullable types as child type of void functions.
I.e. effectively giving void and never the same semantics, except that never also indicates that it never returns.

Additionally I'd be in favour of disallowing (e.g. E_WARNING) consuming the return value of _direct_ calls to void functions (with the exception of standalone direct calls in short closures, because consuming that value is intrinsic rather than necessarily intentional). (Disallowing indirect calls would be detrimental for usage as callback.)

Bob