Hi Alexandre,

On 27/10/2025 08:03, Alexandre Daubois wrote:
Thank you for your interest! I get your concerns about the extension
of such syntax, but we don't feel that there would be a particular
need of extending this syntax. The syntax does not bring something
really new: the behavior described in the RFC already exists in PHP —
we are just making it available at the cast level with explicit null
handling.


I definitely agree that reusing the behaviour from the existing scalar parameter/return checks makes a lot of sense. I also agree that allowing nullable types as the target of the cast is a useful feature.

What I'm not convinced by is that those two features should be combined into one:

$foo = '123abc';
$a = (int)$foo; // OK
$b = (?int)$foo; // not OK!?

It looks like all I've changed is the type I'm casting to, but suddenly I get different behaviour for values that are nothing to do with that change.

Remember that in general contexts, ?int is just short-hand for int|null, and one of the future expansions I can see is adding other unions as cast targets:

(int)$foo;
(?int)$foo;
(int|null)$foo;
(int|float)$foo;
( int | (Countable&SomethingElse) )$foo;

There's no obvious reason for the first one to accept values that the others reject.


The most obvious thing to me is to split the proposal into two, and let people choose how to combine them:

// existing casts
(int)'123abc';     // 123, using existing cast rules
(int)null;     // 0, using existing fallback-to-zero

// nullable types, existing rules
(?int)'123abc';     // 123
(?int)null;     // null

// "failable cast" mode, indicated by !type
(!int)'123abc';    // TypeError
(!int)null;     // TypeError

// nullable type, in failable mode
(!?int)'123abc';     // TypeError
(!?int)null;     // null

"!?" looks a bit ugly, but I'm not convinced by a leading "!" for the new mode anyway, as I'll get into below...


I'm sorry, I think I don't get what you mean. It looks really complex
for something that we would like to keep straightforward.


I agree that it's complex, but the complexity isn't something I'm proposing, it's something I'm *observing*: there are lots of different things people *want* out of cast operators, and we have to decide which ones we're catering for.


As a specific example, last time nullable casts were discussed, some people were hoping for a way to say "cast this to int if valid, and default to null if invalid". In your current proposal, the best they would get is this:

try { $foo = (!int)$_GET['foo']; } catch ( TypeError ) { $foo = null; }


The language is also missing a clean way to get a boolean for whether a particular cast/coercion will succeed - in other words, to validate input against the same rules as the cast. As I mentioned in my last e-mail, exception handling as we currently have it is not well suited for this task either:

try {
    (!int)$_GET['foo'];
} catch ( TypeError ) {
    $validationErrors[] = 'The value for "foo" must be a valid integer';
}

That's a lot of boilerplate, and a lot of run-time overhead, for what could be a simple if statement.


Now, maybe we just say that there are more modes added in future; but if we go with punctuation for modes, we're going to end up with symbol salad:

// target type is "?int"; "!" indicates not to allow values like '123abc'; "@" indicates null-on-error
$foo = (@!?int)$_GET['foo'];

// target type is "?int"; "!" for the validation rules; "~" to return a boolean of whether it would succeed
if ( (~!?int)$_GET['foo'] ) {
    $validationErrors[] = 'The value for "foo" must be a valid integer';
}


That's what I meant by being "easy to extend" - not that your proposal isn't useful, but that other people might want to propose other things later, and we might want a framework to fit them in.


What exactly that should look like I'm not sure, and that's why I haven't shared a full proposal. The main ideas I've been playing with are "SQL style" pseudo-functions with keywords...

$foo = must_cast($_GET['foo'] as int);    // throw on error
$foo = try_cast($_GET['foo'] as int default -1);     // fill with default on error
if ( ! can_cast($_GET['foo'] as int) ) { ... }     // test with same rules
// all available for any type, including nullables:
$foo = must_cast($_GET['foo'] as ?int);
$foo = try_cast($_GET['foo'] as ?int default null);
if ( ! can_cast($_GET['foo'] as ?int) ) { ... }

...or "C++ style" generic/template syntax

$foo = must_cast<int>($_GET['foo']);
$foo = try_cast<int>($_GET['foo'], -1);
if ( ! can_cast<int>($_GET['foo']) ) { ... }
$foo = must_cast<?int>($_GET['foo']);
$foo = try_cast<?int>($_GET['foo'], null);
if ( ! can_cast<?int>($_GET['foo']) ) { ... }

Neither of those looks anything like the current casts, but maybe that's a good thing - we can consider the current loose rules as a legacy feature.

There are obviously plenty of variations, including combining must_cast and try_cast into one keyword, with the behaviour depending on whether a default was provided.


To re-iterate: I'm not saying that your proposal needs to add all possible variations; but concise syntax is a scarce resource, so we should think carefully before adding something we can't re-use for future scope.


Regards,

--
Rowan Tommins
[IMSoP]

Reply via email to