Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Rob Landers


On Thu, Mar 6, 2025, at 23:31, Tim Düsterhus wrote:
> Hi
> 
> On 3/6/25 23:05, Rob Landers wrote:
> >>
> >>   Closure::fromCallable('Outer::Inner::method');
> > 
> > You end up with:
> > 
> > object(Closure)#1 (1) {
> >["function"]=>
> >string(20) "Outer::Inner::method"
> > }
> 
> Okay, does calling the closure work and correctly call the `method` on 
> the inner class? 

Yep, it works!

> The question was intended to make sure that the 
> implementation for callables uses the correct `::` to split. 

This is largely why nesting can only be one level deep. Multiple levels end up 
with far more intrusive changes to the engine and IMHO, severely impact 
readability. I'll update the RFC on this.

> Here's 
> another one that might be interesting:
> 
>  Closure::fromCallable(["Outer::Inner", "method"]);
>  Closure::fromCallable(["Outer", "Inner::method"]);

object(Closure)#1 (1) {
["function"]=>
string(20) "Outer::Inner::method"
}

and 

Uncaught TypeError: Failed to create closure from callable: class "Inner" not 
found

I believe this is correct? I haven't seen this form since my 5.3 days, so I am 
not sure if it is correct or a bug.

> 
> >>   constant('Outer::Inner');
> > 
> > This returns:
> > 
> > string(12) "Outer::Inner"
> 
> Okay, so this behaves as a constant containing the class name. I assume 
> it's with the full namespace if the outer class is namespaced? I'm not 
> sure if I want this to work like this (i.e. whether this should be an 
> error).

That's correct, it contains the fully qualified name. I may have to think on 
this, but I concur that this might be better as an error.

> 
> >>   $inner = 'Inner';
> >>   Outer::{$inner};
> > 
> > This does nothing (but resolves to "Outer::Inner")
> 
> It's consistent with `constant()` and that's good.
> 
> >> … and any other meta-programming functionality working on class
> >> constants or static methods.
> >>
> >> Also, what will happen for:
> >>
> >>   class P {
> >>   class Inner { }
> >>   }
> >>
> >>   class C extends P {
> >>const Inner = 'x';
> >>   }
> >>
> >> (and vice versa)
> > 
> > This is a really good one. If for no other reason than I did a really poor 
> > job of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not 
> > to `C`, so you can do `new C::Inner()` and it will resolve to `P::Inner()`:
> 
> I don't think the RFC explains “resolution” at all. That's why I'm 
> asking with those specific “edge-casey” examples, so that the RFC 
> explicitly spells out the behavior. This is not something that should be 
> “implementation defined”, but something where an explicit design 
> decision has been made.

100% agree. I'll update the RFC after thinking through some points in your 
email.

> 
> I also don't understand why `new C::Inner()` (w|sh)ould resolve to 
> `P::Inner()`. I think my expectation of the code snippet above would be 
> that it is an error.

100% agree. It actually should be an error, according to the RFC. To be honest, 
until I wrote my response to you, I forgot you could redefine constants. So, 
I'll have to take a look at this.

> 
> Likewise, LSP being ignored for inner classes raises an interesting 
> question about the behavior of:
> 
>  class P {
>  class Inner {
>  public function __construct(public string $foo) { }
>  }
> 
>  public static function create() {
>  return new static::Inner('x');
>  }
>  }
> 
>  class C extends P {
>  class Inner {
>  public function __construct(public int $bar) { }
>  }
>  }
> 
> What happens if I call `C::create()`? This should also be specified in 
> the RFC (and tested with a .phpt test).

I'll add a test for it (and detail in the RFC), but it will instantiate a 
C::Inner. This doesn't violate LSP, though, because C::Inner is distinct from 
P::Inner -- they are separate classes and completely unrelated. This is just 
like using static properties or constants.

To explain further, static::Inner is about the same as doing `new 
$class::Inner;`-ish. You are not referencing the same class depending on where 
you are calling the function from. The fact that `static` is a shorthand for 
this is what makes it weird.

I wouldn't be opposed to making `new static::Inner` (or even `new self::Inner`) 
an error, since it is confusing; it would force people to spell out the class 
name. However, I think it is useful when the different inner classes implement 
the same interfaces (explicitly or implicitly). I'd be interested to hear 
thoughts on this, but I'm now wondering if static:: allows casual violations of 
LSP, in general. 🤔

> 
> > As with other static things in PHP, you can do some really strange things 
> > like this. This is similar to how you can redefine static constants in 
> > subclasses.
> 
> We should remove the number of strange things, not add to them.

Truth. 

Thank you for these questions Tim!


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Rob Landers


On Thu, Mar 6, 2025, at 23:20, Ilija Tovilo wrote:
> Hi Rob
> 
> On Thu, Mar 6, 2025 at 12:14 AM Rob Landers  wrote:
> >
> > I'd like to introduce my RFC for discussion: 
> > https://wiki.php.net/rfc/short-and-inner-classes
> 
> Thank you for your proposal.
> 
> I'm very much against the idea of introducing yet another slightly
> shorter form to declare a class. In your examples (they have been
> removed in the meantime), it's unclear how the syntax interacts with
> inherited constructors, trait-used constructors, whether repetition of
> readonly parent properties leads to a "Cannot modify readonly
> property" error, etc.
> 
> The concept of visibility for classes does seem useful to me. Some
> questions that crossed my mind when reading the proposal:
> 
> > Inner classes may only be nested one level deep, may not be a parent class, 
> > and may not be declared abstract
> 
> These restrictions seem somewhat arbitrary. For example, you may want
> a private class to extend another private class, creating some local
> class hierarchy. I think there's value in relaxing this restriction,
> if technically possible.

They're not 100% arbitrary, but mostly due to technical limitations.

- One level deep: Nesting multiple levels results in ambiguous grammar.
- As a parent class: This also results in ambiguity.
- Abstract: If it cannot be a parent class, it doesn't make sense for it to be 
abstract.

> 
> > PHP Fatal error:  Private inner class Box::Point cannot be used in the 
> > global scope
> 
> How is this implemented? I presume using a public nested class as type
> hints should be allowed, but these classes may not be loaded when the
> function is declared. We implement delayed variance checks for
> methods, which do trigger the autoloader, but functions do not (since
> they cannot violate variance rules).

This happens at run time, when the function is called. I believe we can 
guarantee that it will pass/fail with one of the following cases:

- The type check passes and everything is fine
- The type check fails (due to not being the correct type); autoloading never 
happens
- The type check succeeds, but it is private/protected; to have an object of 
that type, you'd have to have loaded the outer class.

This is no different than having:

function foo(MadeUpClass $c) {}

and then never calling foo.

> 
> > Visibility Rules: Private and protected inner classes are only instantiable 
> > within their outer class (or subclasses for protected) and cannot be used 
> > as type declarations outside their outer class. This encapsulation ensures 
> > that the inner class’s implementation details remain within their intended 
> > scope.
> 
> This introduces a weird case where methods with parameter or return
> types referring to private classes may not be redeclared in their
> subclasses, given that the type cannot be specified, even if the
> methods themselves are not private or final. You do mention something
> very similar in your e-mail with the __constructor case, but I really
> fail to see how this provides any benefit.

Yes. This is correct and inline with other languages with inner types. The idea 
behind it is to encapsulate behavior and hide information. The idea is that 
external code should not know anything about the internal structure. This 
grants control over inheritance, which you pointed out, though it is better to 
use final to do that. However, it could be useful if you wanted to create a 
class meant to be inherited (such as a url parser) but prevent modification to 
important methods via inheritance.

> 
> I would also like to echo what has been said about the :: operator,
> which feels out of place. I understand that \ comes with additional
> autoloading challenges, namely requiring a fallback autoloading
> strategy that currently does not conform to PSR-4.

It felt natural to me at the time, but I suspect there may be a better 
nomenclature; we just need to discover it.

> 
> Disclaimer: I have not looked at the implementation at all yet.
> 
> Ilija
> 

— Rob

Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Ilija Tovilo
Hi Rob

On Thu, Mar 6, 2025 at 12:14 AM Rob Landers  wrote:
>
> I'd like to introduce my RFC for discussion: 
> https://wiki.php.net/rfc/short-and-inner-classes

Thank you for your proposal.

I'm very much against the idea of introducing yet another slightly
shorter form to declare a class. In your examples (they have been
removed in the meantime), it's unclear how the syntax interacts with
inherited constructors, trait-used constructors, whether repetition of
readonly parent properties leads to a "Cannot modify readonly
property" error, etc.

The concept of visibility for classes does seem useful to me. Some
questions that crossed my mind when reading the proposal:

> Inner classes may only be nested one level deep, may not be a parent class, 
> and may not be declared abstract

These restrictions seem somewhat arbitrary. For example, you may want
a private class to extend another private class, creating some local
class hierarchy. I think there's value in relaxing this restriction,
if technically possible.

> PHP Fatal error:  Private inner class Box::Point cannot be used in the global 
> scope

How is this implemented? I presume using a public nested class as type
hints should be allowed, but these classes may not be loaded when the
function is declared. We implement delayed variance checks for
methods, which do trigger the autoloader, but functions do not (since
they cannot violate variance rules).

> Visibility Rules: Private and protected inner classes are only instantiable 
> within their outer class (or subclasses for protected) and cannot be used as 
> type declarations outside their outer class. This encapsulation ensures that 
> the inner class’s implementation details remain within their intended scope.

This introduces a weird case where methods with parameter or return
types referring to private classes may not be redeclared in their
subclasses, given that the type cannot be specified, even if the
methods themselves are not private or final. You do mention something
very similar in your e-mail with the __constructor case, but I really
fail to see how this provides any benefit.

I would also like to echo what has been said about the :: operator,
which feels out of place. I understand that \ comes with additional
autoloading challenges, namely requiring a fallback autoloading
strategy that currently does not conform to PSR-4.

Disclaimer: I have not looked at the implementation at all yet.

Ilija


Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Rowan Tommins [IMSoP]

On 06/03/2025 07:49, Edmond Dantes wrote:

> Defining new syntax would encourage us to define a minimum top-level
> behaviour, such as "inside an async{} block, these things are possible,
> and these things are guaranteed to be true"

True. This is precisely the main reason not to change the syntax. The 
issue is not even about how many changes need to be made in the code, 
but rather about how many agreements need to be considered.



Quite the opposite: with a function-and-object approach everything needs 
a name, an API, and a way of being described in relation to how the 
language already works. In a syntax-and-semantics approach, we only need 
to describe the things people actually need.


The generator implementation doesn't have a name or API for where the 
value on the right of a "yield" goes, or where the value on its left 
comes from; we just describe the behaviour: values passed to yield 
somehow end up in the calling scope's Generator object, and values 
passed to that object somehow end up back at the yield statement. We 
don't have to define the API for a GeneratorContext object, and the 
semantics of what happens when users pass it around and store it in 
different scopes.


In the same way, do we actually need to design what an "async context" 
looks like to the user? Do we actually want the user to be able to have 
access to two (nested) async contexts at once, and choose which one to 
spawn a task into? Or would we prefer, at least in the minimum 
implementation, to say "when you spawn a task, it spawns in the current 
async context, and if there is no current async context, an error is 
thrown"?


--
Rowan Tommins
[IMSoP]


Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Daniil Gentili

> Of course, this is not an elegant solution, as it adds one more rule to the 
> language, making it more complex. However, from a legacy perspective, it 
> seems like a minimal scar.
> (to All: Please leave your opinion if you are reading this )
> 
Larry’s approach seems like a horrible idea to me: it increases complexity, 
prevents easy migration of existing code to an asynchronous model and is 
incredibly verbose for no good reason.

The arguments mentioned in 
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
 are not good arguments at all, as they essentially propose explicitly reducing 
concurrency (by allowing it only within async blocks) or making it harder to 
use by forcing users to pass around contexts (which is even worse than function 
colouring 
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).
This (supposedly) reduces issues with resource contention/race conditions: 
sure, if you don’t use concurrency or severely limit it, you will have less 
issues with race conditions, but that’s not an argument in favour of nurseries, 
that’s an argument against concurrency.

Race conditions and deadlocks are possible either way when using concurrency, 
and the way to avoid them is to introduce synchronisation primitives (locks, 
mutexes similar to the ones in https://github.com/amphp/sync/, or lockfree 
solutions like actors, which I am a heavy user of), not bloating signatures by 
forcing users to pass around contexts, reducing concurrency and completely 
disallowing global state.

Golang is the perfect example of a language that does colourless, (mostly) 
contextless concurrency without the need for coloured (async/await keywords) 
functions and other complications.
Race conditions are deadlocks are avoided, like in any concurrent model, by 
using appropriate synchronisation primitives, and by communicating with 
channels (actor model) instead of sharing memory, where appropriate.

Side note, I *very* much like the current approach of implicit cancellations, 
because they even remove the need to pass contexts to make use of 
cancellations, like in golang or amphp (though the RFC could use some further 
work regarding cancellation inheritance between fibers, but that’s a minor 
issue).

> Yeah, so basically, you're creating the service again and again for each 
> coroutine if the coroutine needs to use it. This is a good solution in the 
> context of multitasking, but it loses in terms of performance and memory, as 
> well as complexity and code size, because it requires more factory classes.
> 
^ this

Regarding backwards compatibility (especially with revolt), since I also 
briefly considered submitting an async RFC and thought about it a bit, I can 
suggest exposing an event loop interface like 
https://github.com/revoltphp/event-loop/blob/main/src/EventLoop.php, which 
would allow userland event loop implementations to simply switch to using the 
native event loop as backend (this’ll be especially simple to do for which is 
the main user of fibers, revolt, since the current implementation is clearly 
inspired by revolt’s event loop). 

Essentially, the only thing that’s needed for backwards-compatibility in most 
cases is an API that can be used to register onWritable, onReadable callbacks 
for streams and a way to register delayed (delay) tasks, to completely remove 
the need to invoke stream_select.

I’d recommend chatting with Aaron to further discuss backwards compatibility 
and the overall RFC: I’ve already pinged him, he’ll chime in once he has more 
time to read the RFC.

~~~

To Edmond, as someone who submitted RFCs before: stand your ground, try not to 
listen too much to what people propose in this list, especially if it’s 
regarding radical changes like Larry's; avoid bloating the RFC with proposals 
that you do not really agree with.


Regards,
Daniil Gentili

—

Daniil Gentili - Senior software engineer 

Portfolio: https://daniil.it 
Telegram: https://t.me/danogentili

Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Daniil Gentili

> Of course, this is not an elegant solution, as it adds one more rule to the 
> language, making it more complex. However, from a legacy perspective, it 
> seems like a minimal scar.
> (to All: Please leave your opinion if you are reading this )
> 
Larry’s approach seems like a horrible idea to me: it increases complexity, 
prevents easy migration of existing code to an asynchronous model and is 
incredibly verbose for no good reason.

The arguments mentioned in 
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
 are not good arguments at all, as they essentially propose explicitly reducing 
concurrency (by allowing it only within async blocks) or making it harder to 
use by forcing users to pass around contexts (which is even worse than function 
colouring 
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).
This (supposedly) reduces issues with resource contention/race conditions: 
sure, if you don’t use concurrency or severely limit it, you will have less 
issues with race conditions, but that’s not an argument in favour of nurseries, 
that’s an argument against concurrency.

Race conditions and deadlocks are possible either way when using concurrency, 
and the way to avoid them is to introduce synchronisation primitives (locks, 
mutexes similar to the ones in https://github.com/amphp/sync/, or lockfree 
solutions like actors, which I am a heavy user of), not bloating signatures by 
forcing users to pass around contexts, reducing concurrency and completely 
disallowing global state.

Golang is the perfect example of a language that does colourless, (mostly) 
contextless concurrency without the need for coloured (async/await keywords) 
functions and other complications.
Race conditions are deadlocks are avoided, like in any concurrent model, by 
using appropriate synchronisation primitives, and by communicating with 
channels (actor model) instead of sharing memory, where appropriate.

Side note, I *very* much like the current approach of implicit cancellations, 
because they even remove the need to pass contexts to make use of 
cancellations, like in golang or amphp (though the RFC could use some further 
work regarding cancellation inheritance between fibers, but that’s a minor 
issue).

> Yeah, so basically, you're creating the service again and again for each 
> coroutine if the coroutine needs to use it. This is a good solution in the 
> context of multitasking, but it loses in terms of performance and memory, as 
> well as complexity and code size, because it requires more factory classes.
> 
^ this

Regarding backwards compatibility (especially with revolt), since I also 
briefly considered submitting an async RFC and thought about it a bit, I can 
suggest exposing an event loop interface like 
https://github.com/revoltphp/event-loop/blob/main/src/EventLoop.php, which 
would allow userland event loop implementations to simply switch to using the 
native event loop as backend (this’ll be especially simple to do for which is 
the main user of fibers, revolt, since the current implementation is clearly 
inspired by revolt’s event loop). 

Essentially, the only thing that’s needed for backwards-compatibility in most 
cases is an API that can be used to register onWritable, onReadable callbacks 
for streams and a way to register delayed (delay) tasks, to completely remove 
the need to invoke stream_select.

I’d recommend chatting with Aaron to further discuss backwards compatibility 
and the overall RFC: I’ve already pinged him, he’ll chime in once he has more 
time to read the RFC.

~~~

To Edmond, as someone who submitted RFCs before: stand your ground, try not to 
listen too much to what people propose in this list, especially if it’s 
regarding radical changes like Larry's; avoid bloating the RFC with proposals 
that you do not really agree with.


Regards,
Daniil Gentili

—

Daniil Gentili - Senior software engineer 

Portfolio: https://daniil.it 
Telegram: https://t.me/danogentili


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Michał Marcin Brzuchalski
Hi Rob,

czw., 6 mar 2025 o 00:16 Rob Landers  napisał(a):

> Hello PHP Internals,
>
> I'd like to introduce my RFC for discussion:
> https://wiki.php.net/rfc/short-and-inner-classes
>
> This RFC defines a short class syntax as well as the ability to nest
> classes inside another class. This introduces an unprecedented amount of
> control, flexibility, and expressiveness over how objects are used and
> instantiated in PHP. There is a PR (
> https://github.com/php/php-src/pull/17895) that implements this
> functionality -- all test failures are related to different/new/incorrect
> error messages being generated. However, the core functionality exists to
> take for a test ride.
>
> So, what do I mean by "unprecedented amount of control"? With this change,
> you can declare an inner class as private or protected, preventing its
> usage outside of the outer class:
>
> class User {
>   private class Id {}
>
>   public function __construct(public self::Id $id) {}
> }
>
> In the above example, the class `User` is impossible to construct even
> though it has a public constructor (except through reflection) because
> User::Id is private; User::Id cannot be instantiated, used as a type hint,
> or even via `instanceof` outside of the User class itself. This example
> isn't practical but demonstrates something that is nearly impossible in
> previous versions of PHP, where all classes are essentially publicly
> accessible from anywhere within the codebase.
>
> As a number of inner classes will probably be used as DTOs, the RFC
> introduces a "short syntax" for declaring classes, which enhances
> expressiveness, even allowing the usage of traits, all in a single line:
>
> // declare a readonly Point, that implements Vector2 and uses the
> Evolvable trait
> readonly class Point(public int $x, public int $y) implements Vector2 use
> Evolvable;
>
> When combined with inner classes, it looks something like this:
>
> class Pixel {
>   public readonly class Point(public int $x, public int $y) implements
> Vector2 use Evolvable;
> }
>
> // Create a new pixel point with property $x and $y set to 0
> $p = new Pixel::Point(0, 0);
>
> There are far more details in the RFC itself, so please check it out. I'm
> quite excited to hear your thoughts!
>
> — Rob
>
> PS. I know I tend to rush into things, but I want to make it clear that
> I'm not rushing this -- I've learned from my mistakes (thank you to those
> who have given me advice). I'm going to do this right.
>

Inner classes - YES,
Short classes - YES,
Short empty classes - YES - very nice solution for extending Exception
class, maybe there is no need for parentheses??,
Traits in single line - dunno, personally don't see the need so no much
preference here, IMO would be debatable.

Especially both together. Inner classes are something I was thinking about
many years ago [1].
And the short classes are something that is also in my field of interest. I
remember this was proposed some time ago on ML.

Currently, all of the classes replace array-shapes I mark with additional
docblock including `@internal` tag.
Many DTO's have no logic or not that much, meaning they don't need methods.

An example which you may think could be expressed differently but since I'm
used to expressing my opinion as fast as possible this is what came to my
mind now. Possibly not all of the inner classes should be inner, but there
is one that definitely should be, see:

final class TZParser
{
/**
 * @param TZTypeInfo[] $types
 * @param TTInfo[] $transitions
 * @param string[] $abbreviations
 * @param array $leapSecondData
 */
  public class TZInfo(
string $version,
bool $isV2Plus,
array $types,
array $transitions,
array $abbreviations,
array $leapSecondData,
string|null $posixString,
  );

  protected class TZTypeInfo(public int $gmtOffset, bool $isDst, int
$abbreviationIndex, bool $isStd = false, bool $isUt = false)

  protected class TTInfo(int $timestamp, int $typeIndex);

  private class TZInfoSection(array $types, array $transitions, array
$abbreviations, array $leaps, int $newOffset);

  public static function parse(string $filename): TZInfo
  { /** impl */ }
}

While it looks different with short class and doc block for TZInfo, for the
others that don't require docblock this is pretty nice, even if the fields
would be declared each in a separate line - it's a personal preference.
Additionally, I could easily remove the some prefixes

I see this way of declaring DTO's more convenient than having 3 separate
files for just a small DTO class where I need to put `/** @internal */` on
each of them. Inner classes won't be possible to instantiate outside of the
TZParser class which is intended here. All instances should be available
outside of the parser to read the information with one exception for
TZInfoSection.

In this example the TZInfoSection class is used internally (which is why
marked as private) by the parser itself, it's not exposed outside becau

Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Edmond Dantes
>  In a syntax-and-semantics approach, we only need to describe the things
people actually need.

There is no doubt that syntax provides the programmer with a clear tool for
expressing intent.

> In the same way, do we actually need to design what an "async context"
looks like to the user?

Its implementation is more about deciding which paradigms we want to
support.

If we want to support global services that require local state within a
coroutine, then they need a context. If there are no global "impure"
services (i.e., those maintaining state within a coroutine), then a context
may not be necessary. The first paradigm is not applicable to pure
multitasking—almost all programming languages (as far as I know) have
abandoned it in favor of ownership/memory passing. However, in PHP, it is
popular.

For example, PHP has functions for working with HTTP. One of them writes
the last received headers into a "global" variable, and another function
allows retrieving them. This is where a context is needed. Or, for
instance, when a request is made inside a coroutine, the service that
handles socket interactions under the hood must:

   1. Retrieve a socket from the connection pool.
   2. Place the socket in the coroutine’s context for as long as it is
   needed.

However, this same scenario could be implemented more elegantly if PHP code
explicitly used an object like "Connection" or "Transaction" and retrieved
it from the pool. In that case, a context would not be needed.

Thus, the only question is: do we need to maintain state between
function/method calls within a coroutine?

> Do we actually want the user to be able to have access to two (nested)
async contexts at once, and choose which one to spawn a task into?

If we discard the Go model, where the programmer decides what to do and
which foot to shoot themselves in, and instead use parent-child coroutines,
then such a function breaks this rule. This means it should not exist, as
its presence increases system complexity. However, in the parent-child
model, there is a case where a coroutine needs to be created in a different
context.

For example:

   - A request to reset a password arrives at the server.
   - The API creates a coroutine in a separate context from the request to
   send an email.
   - The API returns a 201 response.

In this case, a special API is needed to accomplish this. The downside of
any strict semantics is the presence of exceptional cases. However, such
cases should be rare. If they are not, then the parent-child model is not
suitable.

To resolve this issue, we need to know the opinions of framework
maintainers. They should say either: *Yes, this approach will reduce the
amount of code*, or *No, it will increase the codebase*, or *We don't care,
do as you like* :)


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Juris Evertovskis

On 2025-03-06 10:04, Tim Düsterhus wrote:


- I don't understand the use of `private` properties. Given that the
classes cannot have methods, they would be inaccessible, no?


I think the RFC was a bit unclear on this. Short classes can have 
methods. The short syntax just doesn't provide the ability to define 
them whilst defining the class.


But otherwise they are indistinguishable from normal classes and they 
can have methods by inheriting them or by using traits.


BR,
Juris


Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Edmond Dantes
Hello, Daniil.

> Essentially, the only thing that’s needed for backwards-compatibility in
most cases is an API that can be used to register onWritable,
> onReadable callbacks for streams and a way to register delayed (delay)
tasks, to completely remove the need to invoke stream_select.

Thank you for this point. It seems I was mistaken in thinking that there is
a Scheduler inside Revolt. Of course, if we're only talking about the
EventLoop, maintaining compatibility won't be an issue at all.

> I’d recommend chatting with Aaron to further discuss backwards
compatibility and the overall RFC: I’ve already pinged him, he’ll chime in
once he has more time to read the RFC.

That would be really cool.

> To Edmond, as someone who submitted RFCs before: stand your ground, try
not to listen too much to what people propose in this list,
> especially if it’s regarding radical changes like Larry's; avoid bloating
the RFC with proposals that you do not really agree with.

Actually, I agree in many ways. In programming, there's an eternal struggle
between abstraction and implementation,

between strict rules and flexibility, between paternalism where the
language makes decisions for you and freedom.

Each of these traits is beneficial in certain scenarios. The most important
thing is to understand whether it will be beneficial for PHP scenarios.
This is the main goal of this RFC stage. That's why I would really like to
hear the voices of those who create PHP's code infrastructure. I mean,
Symfony, Laravel, etc.

Thanks!

Ed.


Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Edmond Dantes
>  One key question, if we disallow explicitly creating Fibers inside an
async block,
> can a Fiber be created outside of it and not block async, or would that
also be excluded?  Viz, this is illegal:
>

Creating a `Fiber` outside of an asynchronous block is allowed; this
ensures backward compatibility.
According to the logic integrity rule, an asynchronous block cannot be
created inside a Fiber. This is a correct statement.

However, if the asynchronous block blocks execution, then it does not
matter whether a Fiber was created or not, because it will not be possible
to switch it in any way.
So, the answer to your question is: yes, such code is legal, but the Fiber
will not be usable for switching.

In other words, Fiber and an asynchronous block are mutually exclusive.
Only one of them can be used at a time: either Fiber + Revolt or an
asynchronous block.

Of course, this is not an elegant solution, as it adds one more rule to the
language, making it more complex. However, from a legacy perspective, it
seems like a minimal scar.

(to All: Please leave your opinion if you are reading this )

>  // This return statement blocks until foo() and bar() complete.

Yes, *that's correct*. That's exactly what I mean.

Of course, under the hood, return will execute immediately if the coroutine
is not waiting for anything. However, the Scheduler will store its result
and pause it until the child coroutines finish their work.

In essence, this follows the parent-child coroutine pattern, where they are
always linked. The downside is that it requires more code inside the
implementation, and some people might accuse us of a paternalistic
approach. :)

>
> should consider if async {} makes sense to have its own catch and finally
blocks built in.)
>
We can use the approach from the RFC to catch exceptions from child
coroutines: explicit waiting, which creates a handover point for exceptions.

Alternatively, a separate handler like Context::catch() could be
introduced, which can be defined at the beginning of the coroutine.

Or both approaches could be supported. There's definitely something to
think about here.

>
>  That is still dependency injection, because ThingRunner is still taking
all of its dependencies via the constructor.  And being readonly, it's
still immutable-friendly.
>

Yeah, so basically, you're creating the service again and again for each
coroutine if the coroutine needs to use it. This is a good solution in the
context of multitasking, but it loses in terms of performance and memory,
as well as complexity and code size, because it requires more factory
classes.

The main advantage of *LongRunning* is initializing once and using it
multiple times. On the other hand, this approach explicitly manages memory,
ensuring that all objects are created within the coroutine's context rather
than in the global context.

Ah, now I see how much you dislike global state! :)

However, in a scenario where a web server handles many similar requests,
"global state" might not necessarily win in terms of speed but rather due
to the simplicity of implementation and the overall maintenance cost of the
code. (I know that in programming, there is an entire camp of immutability
advocates who preach that their approach is the key remedy for errors.)

I would support both paradigms, especially since it doesn’t cost much.

A coroutine will own its internal context anyway, and this context will be
carried along with it, even across threads. How to use this context is up
to the programmer to decide.  But at the same time, I will try to make the
pattern you described fit seamlessly into this logic.

Ed.


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Tim Düsterhus

Hi

On 3/6/25 20:08, Niels Dossche wrote:

What I'm less in favor of is the implementation choice to expose the inner 
class as a property/const and using a fetch mode to grab it.
That feels quite weird to me honestly. How did you arrive at this choice?


Somewhat relatedly, the RFC does not mention how the choice of `::` as 
the separator interacts with the following features (i.e. what will the 
result of each of the statements be):


Closure::fromCallable('Outer::Inner::method');
new ReflectionMethod('Outer::Inner::method');
defined('Outer::Inner');
constant('Outer::Inner');
$inner = 'Inner';
Outer::{$inner};

… and any other meta-programming functionality working on class 
constants or static methods.


Also, what will happen for:

class P {
class Inner { }
}

class C extends P {
 const Inner = 'x';
}

(and vice versa)

Best regards
Tim Düsterhus


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Rob Landers
On Thu, Mar 6, 2025, at 22:00, Tim Düsterhus wrote:
> Hi
> 
> On 3/6/25 20:08, Niels Dossche wrote:
> > What I'm less in favor of is the implementation choice to expose the inner 
> > class as a property/const and using a fetch mode to grab it.
> > That feels quite weird to me honestly. How did you arrive at this choice?
> 
> Somewhat relatedly, the RFC does not mention how the choice of `::` as 
> the separator interacts with the following features (i.e. what will the 
> result of each of the statements be):

Sorry to double post, but before updating the RFC, I figure I'll go ahead and 
answer here and see if it is what you expect.

> 
>  Closure::fromCallable('Outer::Inner::method');

You end up with:

object(Closure)#1 (1) {
  ["function"]=>
  string(20) "Outer::Inner::method"
}

>  new ReflectionMethod('Outer::Inner::method');

The current implementation returns an error here, but it should give you an 
actual ReflectionMethod. I'll have to take a look and see what is going on.

>  defined('Outer::Inner');

This returns: `true`.

>  constant('Outer::Inner');

This returns:

string(12) "Outer::Inner"

>  $inner = 'Inner';
>  Outer::{$inner};

This does nothing (but resolves to "Outer::Inner")

> … and any other meta-programming functionality working on class 
> constants or static methods.
> 
> Also, what will happen for:
> 
>  class P {
>  class Inner { }
>  }
> 
>  class C extends P {
>   const Inner = 'x';
>  }
> 
> (and vice versa)

This is a really good one. If for no other reason than I did a really poor job 
of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not to `C`, 
so you can do `new C::Inner()` and it will resolve to `P::Inner()`:

object(P::Inner)#2 (0) {
}

As with other static things in PHP, you can do some really strange things like 
this. This is similar to how you can redefine static constants in subclasses.

My main goal is to prevent exactly this type of confusion:

new C::Inner() 
vs. 
echo C::Inner.

> Somewhat relatedly, the RFC does not mention how the choice of `::` as 
> the separator

This felt the most natural to me, but as with all syntax on this list, I'd be 
interested in other colors to paint the bikeshed! Especially ones that I may 
not have previously considered!

— Rob

Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Rowan Tommins [IMSoP]

On 06/03/2025 11:31, Edmond Dantes wrote:
For example, PHP has functions for working with HTTP. One of them 
writes the last received headers into a "global" variable, and another 
function allows retrieving them. This is where a context is needed.



OK, let's dig into this case: what is the actual problem, and what does 
an async design need to provide so that it can be solved.


As far as I know, all current SAPIs follow one of two patterns:

1) The traditional "shared nothing" approach: each request is launched 
in a new process or thread, and all global state is isolated to that 
request.
2) The explicit injection approach: the request and response are 
represented as objects, and the user must pass those objects around to 
where they are needed.


Notably, 2 can be emulated on top of 1, but not vice versa, and this is 
exactly what a lot of modern applications and frameworks do: they take 
the SAPI's global state, and wrap it in injected objects (e.g. PSR-7 
ServerRequestInterface and ServerResponseInterface).


Code written that way will work fine on a SAPI that spawns a fiber for 
each request, so there's no problem for us to solve there.


At the other extreme are frameworks and applications that access the 
global state directly throughout - making heavy use of superglobal, 
global, and static variables; directly outputting using echo/print, etc. 
Those will break in a fiber-based SAPI, but as far as I can see, there's 
nothing the async design can do to fix that.


In the middle, there are some applications we *might* be able to help: 
they rely on global state, but wrap it in global functions or static 
methods which could be replaced with some magic from the async 
implementation.



So our problem statement is:

- given a function that takes no request-specific input, and is expected 
to return request-specific state (e.g. function 
get_query_string_param(string $name): ?string)

- and, given a SAPI that spawns a fiber for each request
- how do we adjust the implementation of the function, without changing 
its signature?



Things we don't need to define:

- how the SAPI works
- how the data is structured inside the function

Non-solutions:

- refactoring the application to pass around a Context object - if we're 
willing to do that, we can just pass around a PSR-7 RequestInterface 
instead, and the problem goes away



Minimal solution:

- a way to get an integer or string, which the function can use to 
partition its data


Usage example:

function get_query_string_param(string $name): ?string {
    global $request_data; // in a shared-nothing SAPI, this is 
per-request; but in a fiber-based one, it's shared between requests
    $request_data_partition = $request_data[ 
Fiber::getCurrent()->getId() ]; // this line makes the function work 
under concurrent SAPIs
    return $request_data_partition['query_string'][$name]; // this line 
is basically unchanged from the original application

}


Limitation:

- if the SAPI spawns a fiber for the request, but that fiber then spawns 
child fibers, the function won't find the right partition


Minimal solution:

- track and expose the "parent" of each fiber

Usage example:

function get_query_string_param(string $name): ?string {
    global $request_data;
    // Traverse until we find the ID we've stored data against in our 
request bootstrapping code

    $fiber = Fiber::getCurrent();
    while ( ! isset($request_data[ $fiber->getId() ] ) {
    $fiber = $fiber->getParent();
    }
    $request_data_partition = $request_data[ $fiber->getId() ];
    return $request_data_partition['query_string'][$name];
}


Obviously, this isn't the only solution, but it is sufficient for this 
problem.


As a first pass, it saves us bikeshedding exactly what methods an 
Async\Context class should have, because that whole class can be added 
later, or just implemented in userland.


If we strip down the solution initially, we can concentrate on the 
fundamental design - things like "Fibers have parents", and what that 
implies for how they're started and used.



--
Rowan Tommins
[IMSoP]


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Tim Düsterhus

Hi

On 3/6/25 22:38, Rob Landers wrote:

I put a lot of thought into this issue off and on, all day. I've decided to 
remove short syntax from the RFC and focus on inner classes.


Good choice.

Don't forget to update the title of the RFC in the Overview page: 
https://wiki.php.net/rfc


Best regards
Tim Düsterhus


Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Tim Düsterhus

Hi

On 3/6/25 23:05, Rob Landers wrote:


  Closure::fromCallable('Outer::Inner::method');


You end up with:

object(Closure)#1 (1) {
   ["function"]=>
   string(20) "Outer::Inner::method"
}


Okay, does calling the closure work and correctly call the `method` on 
the inner class? The question was intended to make sure that the 
implementation for callables uses the correct `::` to split. Here's 
another one that might be interesting:


Closure::fromCallable(["Outer::Inner", "method"]);
Closure::fromCallable(["Outer", "Inner::method"]);


  constant('Outer::Inner');


This returns:

string(12) "Outer::Inner"


Okay, so this behaves as a constant containing the class name. I assume 
it's with the full namespace if the outer class is namespaced? I'm not 
sure if I want this to work like this (i.e. whether this should be an 
error).



  $inner = 'Inner';
  Outer::{$inner};


This does nothing (but resolves to "Outer::Inner")


It's consistent with `constant()` and that's good.


… and any other meta-programming functionality working on class
constants or static methods.

Also, what will happen for:

  class P {
  class Inner { }
  }

  class C extends P {
   const Inner = 'x';
  }

(and vice versa)


This is a really good one. If for no other reason than I did a really poor job 
of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not to `C`, 
so you can do `new C::Inner()` and it will resolve to `P::Inner()`:


I don't think the RFC explains “resolution” at all. That's why I'm 
asking with those specific “edge-casey” examples, so that the RFC 
explicitly spells out the behavior. This is not something that should be 
“implementation defined”, but something where an explicit design 
decision has been made.


I also don't understand why `new C::Inner()` (w|sh)ould resolve to 
`P::Inner()`. I think my expectation of the code snippet above would be 
that it is an error.


Likewise, LSP being ignored for inner classes raises an interesting 
question about the behavior of:


class P {
class Inner {
public function __construct(public string $foo) { }
}

public static function create() {
return new static::Inner('x');
}
}

class C extends P {
class Inner {
public function __construct(public int $bar) { }
}
}

What happens if I call `C::create()`? This should also be specified in 
the RFC (and tested with a .phpt test).



As with other static things in PHP, you can do some really strange things like 
this. This is similar to how you can redefine static constants in subclasses.


We should remove the number of strange things, not add to them.

Best regards
Tim Düsterhus


Re: [PHP-DEV] PHP True Async RFC

2025-03-06 Thread Larry Garfield
On Thu, Mar 6, 2025, at 2:52 AM, Edmond Dantes wrote:
>>  One key question, if we disallow explicitly creating Fibers inside an async 
>> block, 
>> can a Fiber be created outside of it and not block async, or would that also 
>> be excluded?  Viz, this is illegal:
>>
> Creating a `Fiber` outside of an asynchronous block is allowed; this 
> ensures backward compatibility.
> According to the logic integrity rule, an asynchronous block cannot be 
> created inside a Fiber. This is a correct statement.
>
> However, if the asynchronous block blocks execution, then it does not 
> matter whether a Fiber was created or not, because it will not be 
> possible to switch it in any way.
> So, the answer to your question is: yes, such code is legal, but the 
> Fiber will not be usable for switching.
>
> In other words, Fiber and an asynchronous block are mutually exclusive. 
> Only one of them can be used at a time: either Fiber + Revolt or an 
> asynchronous block.
>
> Of course, this is not an elegant solution, as it adds one more rule to 
> the language, making it more complex. However, from a legacy 
> perspective, it seems like a minimal scar.
>
> (to All: Please leave your opinion if you are reading this )

This seems like a reasonable approach to me, given the current state.  At any 
give time, you can have "manual" or "automatic" handling in use, but one has to 
completely finish before you can start using the other.  Whether we should 
remove the "manual" access in the future becomes a question for the future.

>>  // This return statement blocks until foo() and bar() complete.
>
> Yes, *that's correct*. That's exactly what I mean.
>
> Of course, under the hood, `return` will execute immediately if the 
> coroutine is not waiting for anything. However, the Scheduler will 
> store its result and pause it until the child coroutines finish their 
> work.
>
> In essence, this follows the parent-child coroutine pattern, where they 
> are always linked. The downside is that it requires more code inside 
> the implementation, and some people might accuse us of a paternalistic 
> approach. :)

See, what you call "paternalistic" I say is "basic good usability."  
Affordances are part of the design of everything.  Good design means making 
doing the right thing easy and the wrong thing hard, preferably impossible.  
(Eg, why 120v and 220v outlets have incompatible plugs, to use the classic 
example.)  I am a strong support of correct by construction / make invalid 
states unrepresentable / type-driven development, or whatever it's called this 
week.  

And history has demonstrated that humans simply cannot be trusted to manually 
handle synchronization safely, just like they cannot be trusted to manually 
handle memory safely. :-)  (That's why green threads et al exist.)

>>  That is still dependency injection, because ThingRunner is still taking all 
>> of its dependencies via the constructor.  And being readonly, it's still 
>> immutable-friendly.
>> 
>
> Yeah, so basically, you're creating the service again and again for 
> each coroutine if the coroutine needs to use it. This is a good 
> solution in the context of multitasking, but it loses in terms of 
> performance and memory, as well as complexity and code size, because it 
> requires more factory classes.

Not necessarily.  It depends on what all you're doing when creating those 
objects.  It can be quite fast.  Plus, if you want a simpler approach, just 
pass the context directly:

async $ctx {
  $ctx->run($httpClient->runAsync($ctx, $url));
}

It's just a parameter to pass.  How you pass it is up to you.

It is literally the same argument for "pass the DB connection into the 
constructor, don't call a static method to get it" or "pass in the current user 
object to the method, don't call a global function to get it."  These are 
decades-old discussions with known solved problems, which all boil down to 
"pass things explicitly."

To quote someone on FP: "The benefit of functional programming is it makes data 
flow explicit.  The downside is it sometimes painfully explicit."

I am far happier with explicit that is occasionally annoyingly so, and building 
tools and syntax to reduce that annoyance, than having implicit data just 
floating around in the ether around me and praying it's what I expect it to be.

> The main advantage of *LongRunning* is initializing once and using it 
> multiple times. On the other hand, this approach explicitly manages 
> memory, ensuring that all objects are created within the coroutine's 
> context rather than in the global context.

As above, in simpler cases you can just make the context a boring old function 
parameter, in which case the perf overhead is unmesurable.

> Ah, now I see how much you dislike global state! :)

It is the root of all evil.

> However, in a scenario where a web server handles many similar 
> requests, "global state" might not necessarily win in terms of speed 
> but rather due to the simplicity of im

Re: [PHP-DEV] RFC: short and inner classes

2025-03-06 Thread Larry Garfield
On Wed, Mar 5, 2025, at 5:11 PM, Rob Landers wrote:
> Hello PHP Internals,
>
> I'd like to introduce my RFC for discussion: 
> https://wiki.php.net/rfc/short-and-inner-classes

I agree with others who have said this should be two RFCs.  They stand alone, 
but can complement each other well.  That's fine, talk about how that works, 
but they're separate RFCs.  (Like we split hooks and aviz.)

I'm broadly in favor of short classes and warm on inner classes, with assorted 
caveats as below.

## Short classes

The no-method-definitions restriction seems odd to me.  I don't really see what 
value that provides.  The main advantages are

1. Even shorter way to define promoted properties for the common case.
2. No need to define a body if you don't need one.

Neither of those conflicts with defining methods.

If the concern is the verbosity of method definitions, well, I tried:

https://wiki.php.net/rfc/short-functions

I still think that would be beneficial, but am frying other fish at the moment.

So really, all it need to do is allow inline definition of the properties by 
the class name as an alternative to a constructor.

class Point(int $x, int $y) {}

class Rect(Point $tl, Point $br) {
  public method area() {
 /* ... */ 
  }
}

(Also allowing to skip the empty {} is fine with me, but that the last of my 
concerns.)

The other question is trait usage.  The RFC proposes just shifting that up to 
the declaration line.  That also seems fine to do either way, regardless of 
whether there are methods.

My biggest concern with this is that it makes methods and short-classes 
mutually incompatible.  So if you have a class that uses short-syntax, and as 
it evolves you realize it needs one method, sucks to be you, now you have to 
rewrite basically the whole class to a long-form constructor.  That sucks even 
more than rewriting a short-lambda arrow function to a long-form closure, 
except without the justification of capture semantics.

Additionally: The RFC doesn't specify if or how properties with hooks are 
supported.  That should be defined so we can argue about it. :-)

Additionally: What happens here:

class Point(int $x, int $y);

class 3DPoint(int $z) extends Point;

I have to redeclare all of the parameters from the parent, just to add one?  
That's ugly. 

## Inner classes

I'm on board with the use case.  What I'm not sure on is inner classes vs 
file-private visibility, something that Ilija was working on at one point and 
Michał Brzuchalski suggested in his post.  Both solve largely the same problem 
with different spelling.

Arguably, inner classes have fewer issues with current autoload conventions.  I 
must ponder this further.

> However, no classes may not inherit from inner classes, but inner classes may 
> inherit from other classes, including the outer class. 

I think you have one too many negatives in that sentence.

--Larry Garfield