Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128976 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 127611A00BC for ; Mon, 27 Oct 2025 12:54:52 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1761569696; bh=tq3VGc87sbA9eHzMU28I+M1ykgAPTzSAPFp6euld+Lc=; h=Date:Subject:To:References:From:In-Reply-To:From; b=fXfNvTrunujdW3yBYkbc2Nmx4xC5oG3xNT2WqGM2shleTGjcqclUC2FG8O/kZeXNy qAs6hJgjEiCpO+4EOcEXOuYT6cGRUPjRDNmqaZG0cEXkHDjLo69GtbYhwZLDlGTcjq TqhFxRamXHMMYMvDt4LY18EWcnmzsb75P9yW/5XZtuGLaLAUOELsaWSjrralcQolJX 5NugmKMV7L0TxFZPy4vobLcPLmsmuITa8Fxvp7pGRISRdx9XD96AEs3VpBLr31GnlA g1hdM2bwneU0N5QvMrFlZgDXcRxc0VNz427s8PyNNL/Mo+kW084/3aN9NVDWUXCQIh D/neGKT7+Q9jw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 56006180056 for ; Mon, 27 Oct 2025 12:54:55 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-2.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from fout-a5-smtp.messagingengine.com (fout-a5-smtp.messagingengine.com [103.168.172.148]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Mon, 27 Oct 2025 12:54:54 +0000 (UTC) Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfout.phl.internal (Postfix) with ESMTP id 546E6EC0346 for ; Mon, 27 Oct 2025 08:54:49 -0400 (EDT) Received: from phl-mailfrontend-02 ([10.202.2.163]) by phl-compute-02.internal (MEProxy); Mon, 27 Oct 2025 08:54:49 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=rwec.co.uk; h=cc :content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:subject :subject:to:to; s=fm3; t=1761569689; x=1761656089; bh=8UnI/pLnWi Kzmw2N8UvzNcR/M9mzA1PJJZHjods07Mg=; b=GBPUBWh8B4LXYhKH+6ucHsHI79 FF6yQERadq+Kirww94rZVOBuHMBSBUpnqOnDlUJWgN6sSfo40QE0A6R6xA1HyuI2 4TOLLlj4NpqLT/H9ScjgX4wefS7gOgRUIp7hdv2BFcQ/6SJ64cSWin2FV09pdzgq t0jG5jlBjOjdeVlQguHcIUOOFTs42LKPJdwuAmI0A6VwJU4riR8ATNUuSTrNmfNc zKlhVZFBxgbfP+Ljtj1zgUo9HRmDQPVRq/Qz3XGgFh+WjuPuRv/pka+weQa1G6F/ sSCr+ij8pOmaPDNbLEwbteJkLqxjyvcHj562xUgiMZUnsBOY3Pp18CjKhhRw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= 1761569689; x=1761656089; bh=8UnI/pLnWiKzmw2N8UvzNcR/M9mzA1PJJZH jods07Mg=; b=Jv5yYSf7CUJbr8AP1vqwSsKjpNuEg62CXaWXcaoTF7L+3rcKDOI CRxN7/eWF6xhtCo7K5wzhQD9FaXsfZz/rIwi+xOrMBUgqqQeyBA3TJYKei2s+OiQ 8FSnmNHGSZhso3ABzmswA+UfeIGl3010x97v0FtLjHFnOSqe7lOIbWxB8t+L46me jZZu/qZdsYF9QRDbbQ9EbBHQN0PfbqFdEl/Et70TrvQTw1OpYkQh3rzTAie3vThu qFPc/gud/3sLFP1HTVFJrm+7Nh8l+nDISoHJPgnyw82iywU6O8xZgxNhS7wS5qRv 2ZedJDnyMa4ALZ28tVahN8KHixUBoBcWOGQ== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggdduheektddvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucenucfjughrpegtkfffgggfuffvfhfhjgesrgdtreertd dvjeenucfhrhhomhepfdftohifrghnucfvohhmmhhinhhsucglkffoufhorfgnfdcuoehi mhhsohhprdhphhhpsehrfigvtgdrtghordhukheqnecuggftrfgrthhtvghrnhepheetle eiiefgueduieeuieffvdevheduueefkeejuefgffeftdeitdegtedtleetnecuvehluhhs thgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepihhmshhophdrphhhph esrhifvggtrdgtohdruhhkpdhnsggprhgtphhtthhopedupdhmohguvgepshhmthhpohhu thdprhgtphhtthhopehinhhtvghrnhgrlhhssehlihhsthhsrdhphhhprdhnvght X-ME-Proxy: Feedback-ID: id5114917:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Mon, 27 Oct 2025 08:54:48 -0400 (EDT) Content-Type: multipart/alternative; boundary="------------LdxnkdiyqlnL37S0WYMJGpIN" Message-ID: Date: Mon, 27 Oct 2025 12:54:47 +0000 Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PHP-DEV] [RFC] Nullable and non-nullable cast operators Content-Language: en-GB To: internals@lists.php.net References: In-Reply-To: From: imsop.php@rwec.co.uk ("Rowan Tommins [IMSoP]") This is a multi-part message in MIME format. --------------LdxnkdiyqlnL37S0WYMJGpIN Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit 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($_GET['foo']); $foo = try_cast($_GET['foo'], -1); if ( ! can_cast($_GET['foo']) ) { ... } $foo = must_cast($_GET['foo']); $foo = try_cast($_GET['foo'], null); if ( ! can_cast($_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] --------------LdxnkdiyqlnL37S0WYMJGpIN Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 8bit
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]
--------------LdxnkdiyqlnL37S0WYMJGpIN--