Re: [PHP-DEV] [Pre-RFC] Scalar object methods (with a working implementation)
[email protected] (Seifeddine Gmati)
| Newsgroups | php.internals |
|---|---|
| Message-ID | <CAFyg4HjO4=6S769YvNsjCXCH5nfRtdkuQDzS_DoVKo+toZTz6A@mail.gmail.com> |
On Mon, 29 Jun 2026 at 23:33, Michal Kral <[email protected]> wrote: > > Hi all, > > Per Seifeddine's suggestion to keep this out of the karma-request > thread, I'm opening a pre-RFC discussion for scalar object methods -- > calling a small, curated set of methods directly on scalar values, e.g. > $str->trim(), (3)->pow(2). There's a complete, tested implementation and > a full write-up (links below); I'd like to surface the strongest > objections before I write the formal RFC. Hi Michal, Thanks for the detailed write-up. I'll be upfront: I'm against this feature. I have two concrete objections to the approach and one broader objection to the idea itself. > The idea: dispatch only on receivers the compiler already knows are > scalar. The method call is rewritten at compile time to an ordinary call > into an internal backing class -- no runtime type dispatch, no new > opcode, the object method-call path is untouched. A receiver qualifies > only if its type is guaranteed syntactically: a literal, a > (string)/(int) cast, a concatenation/interpolation, a non-nullable > scalar-typed property, or a call with a declared non-nullable scalar > return type. This is my main objection, and I think it's a fatal one. Restricting dispatch to receivers "the compiler already knows are scalar" sounds safe, but in practice it covers almost no real code. PHP is compiled one file at a time, with no view into other files. The compiler does not perform whole-program analysis; when compiling a given file, it generally has no knowledge of declarations in other files and haven't been autoloaded yet. So take your own qualifying rule, "a call with a declared non-nullable scalar return type": class Example { public static function getStr(): string { return "x"; } } $x = Example::getStr(); $x->length(); A human reads this and knows `$x` is a string. But the PHP *compiler*, when compiling the file that contains `$x = Example::getStr()`, only knows `getStr()` returns `string` if `Example` happens to be declared in the same file. Move `Example` into its own autoloaded file (which is how essentially all real code is organised) and the compiler has no idea what `getStr()` returns at compile time. So `$x->length()` would *not* dispatch, even though the type is completely determined. The result is that the same expression works or fails depending on whether a class is in the same file or autoloaded from another one. That's not a predictable rule a developer can hold in their head. Real-world values emerge from call chains, conditionals, and cross-file boundaries, The cases the compiler can't prove syntactically. The feature ends up usable only on literals and casts and almost nothing else. This also forces special handling in static-analysis tools because the compiler and the analyser will disagree about the same line. Given `$x->length()`, PHPStan/Psalm/Mago will happily infer `$x` is a string and accept it, while the compiler rejects it. > - The backing classes are internal-only (NUL-prefixed name, like > anonymous classes): class_exists('Str') is false, no Reflection, > userland "class Str {}" can't collide. My second objection. If `$s->length()` is sugar for `Str::length($s)`, then `Str` (or whatever backs it) *must* be visible to userland in some form. Static analysers (PHPStan, Psalm, Mago, Phan, PhpStorm, ...) need a definition describing which methods exist, to type-check calls, support "go to definition," report wrong arity, and the PHP-based ones need it to be reflectable. The collision concern you're solving with NUL-prefixing isn't worth that cost. Userland already has thousands of `Str` classes; the clean fix is to namespace the backing classes (or simply pick non-colliding names), not to hide them from the entire ecosystem. Solve the naming problem with naming, not by blinding the tooling. (Related, and unaddressed in the write-up: what happens with methods that take arguments, e.g. `$s->indexOf($y)`? Does an arity mismatch fail at compile time, or at runtime with `ArgumentCountError` like a normal method call?) Finally, the broader point. Even if both of the above were fully resolved, I'd still be against this. PHP has an established way of doing these operations and scalar methods don't remove that. `trim($s)`, `mb_trim($s)`, and `$s->trim()` would all coexist, with the method form available only sometimes. To me that's too disruptive to the norm for what it buys: it doesn't replace anything, it doesn't compose cleanly, and it introduces a method-call syntax on values that carry no object identity. I don't think the language is better for it. Cheers, Seifeddine.