[PHP-DEV] [RFC] Partial Function Application for instance of non-static methods ("$this")

On Friday, February 6th, 2026 at 21:32, Larry Garfield <larry@garfieldtech.com> wrote:

On Fri, Feb 6, 2026, at 10:43 AM, Tim Düsterhus wrote:
> Hi
>
> On 1/22/26 17:41, Larry Garfield wrote:
>> More spitballing on my previous reply:
>>
>> class Test {
>> public function stuff(int $a) {}
>> }
>>
>> (Test)?->stuff(?);
>
> As mentioned in the sibling mail, this is existing syntax and thus
> doesn't work.

Sure, but we can fiddle with the details to find something that works. I also suggested something like these to Arnaud off-list:

(?: Test)->stuff(?);
(Test ?)->stuff(?);
((Test)?)->stuff(?);

> Keeping full type information is the main benefit of PFA over “just
> write a Closure”. Being able to reorder parameters as part of partial
> application is another explicit feature that would not be supported by
> that syntax.

Honestly, I don't much care about the reordering. And with this approach you'd still be able to reorder the method params, just keep the object as first arg.

>> That would then be a lot easier to write in cases where you're just dropping a $this->stuff() call into a pipe chain but want to receive $this.
>
> The use case for partially applying `$this` is in cases where you need a
> “function handle”, that's why the examples are ones where the resulting
> Closure is passed as a parameter to another function.
>
> Within a pipe chain you would just use the regular `->` operator on the
> result of the previous step:
>
> $result = (trim($username)
> |> $repository->findBy(name: ?)
> )->getId();
>
> It would also naturally support `?->` in case your repository returns
> `null` when the user cannot be found.

Directly in a pipe chain, sure. However, I would see this as most useful as a callback to array_map et al (or future equivalents).

Eg:

import_stuff()
  |> array_filter(?, (?)->hasField('comment')
  |> array_map((?)->save(), $records)
;

In this case, the only thing being partialed is the object on which to invoke. So there's no need to think about reordering the params in the first place: There's just one.

Needing to specify the type in this case:

import_stuff()
  |> array_filter(?, Record::hasField('comment', ...)
  |> array_map(Record::save(...), $records)
;

Just adds more noise, and IMO creates confusion between static and non-static methods.

--Larry Garfield

I find the `(Test ?)->method()` suggestion quite nice. For 2 reasons, it looks similar to a function argument ("Type $arg"), and following that, given that the type is optional for function arguments, it could also be optional here to have `(?)->method()` or even `?->method()` as a shortcut.

I understand it means some optimization cannot be done, but in a lot of case it doesn't matter, and if I have to write `DateTimeImmutable::format($this: ?, 'c')` vs `fn ($dt) => $dt->format('c')`. The only benefit would be the optimization. so if I don't care about that I'm still going to go for the shorter one unless this a performance critical situation

Having `(DateTimeImmutable ?)->format('c')` and `?->format('c')` as a non-optimized shortcut seems more interesting to me.

I'm not too bothered by the reordering, I think this still covers 99% of the use cases

---

That said, regarding your 2 suggestions, Tim, and after reading your explanations, my preference goes for `$this: ?`

And yet more suggestions: `DateTimeImmutable::methodName($: ?, ...)`

and whatever is between `$` and `:` could be used as the arg name of the generated function (with something like "__this" being the default if omitted)

the `$` prefix could also be replaced by any symbol not authorized at the start of an argument name, eg:

* `DateTimeImmutable::methodName(*it: ?, ...)`
* `DateTimeImmutable::methodName(~it: ?, ...)`
* `DateTimeImmutable::methodName(.it: ?, ...)` (it's getting worse and worse, ain't it?)

or, even more drastic changes:

* `DateTimeImmutable::methodName(*it, ...)`
* `DateTimeImmutable::methodName(?this, ...)`

(feel free to silently ignore them; I'm not putting that much thought into those suggestions)

regards,

Mathieu Rochette

Hey Tim, I think you jumped to a conclusion without exploring the space of possibilities fully:

On 6.2.2026 16:12:05, Tim Düsterhus wrote:

With regard to your comment on the PR and the “Open Issue” listed in the RFC about the name for the `$this` parameter in the resulting Closure we have two options to offer:

1. `ClassName::methodName(this: ?)` with the parameter being called `$__this`.

2. `ClassName::methodName($this: ?)` with the parameter being called `$this`.

Notably `ClassName::methodName(this: ?)` with the parameter being called `$this` is not an option, because it would result in inconsistent behavior:

`ClassName::methodName(this: ?)` semantically relies on `$this` never being a valid parameter name, such that `this: ?` can unambiguously refer to the “implicit `$this` value” for a method call. However when the parameter in the generated Closure would be called `$this`, there is some ambiguity for cases like:

 $c = DateTime::format\(this: ?, format: ?\);
 $c2 = $c\(this: ?, format: &#39;c&#39;\);

Is `this: ?` in the definition of `$c2` referring to the `$this` parameter of the generated Closure or is it an attempt to partially apply the `Closure` object for the `Closure::__invoke()` method that is referenced by `$c`?

Similarly allowing `this: $object` with a concrete value is explicitly disallowed, because of possible ambiguity with regard to inheritance:

 class P \{ public function m\(\) \{ echo &quot;P&quot;; \} \}
 class C extends P \{ public function m\(\) \{ echo &quot;C&quot;; \} \}

 // is this calling P::m\(\) or is this calling C::m\(\)?
 P::m\(this: new C\(\)\);

This would however prevent calling a partially applied instance method with named arguments:

 $c = DateTime::format\(this: ?, format: ?\);
 // Disallowed, because this: must be partially applied\.
 $c\(this: new DateTime\(\), format: &#39;c&#39;\);

If the syntax to define the PFA was using `C::m($this: ?)`, `$this` in the resulting Closure would just work like any other parameter.
1. `ClassName::methodName(this: ?)` with the parameter being called `$__this`.

2. `ClassName::methodName($this: ?)` with the parameter being called `$this`.

Allowing P::m(this: $object) with a concrete value should behave identically to P::m(this: ?)($object), which in turn should behave identically to $object->m() if called outside of C (or its children), otherwise equivalently to `private function forwardM() { return P::m(); } $object->forwardM();` (i.e. a (grand)parent class method can always be called).

This approach is consistent with how method calling works in child classes.

More normatively for any class P and method m, P::m(this: ?) needs to store the class where the PFA closure is created as called_scope on the Closure when that class is instanceof P unless P::m is abstract, so that any call to the resulting closure is checked against that called_scope: If the $this object the Closure is ultimately called with is instanceof the called_scope of the Closure, the specific given method must be called (i.e. specifically P::m()), otherwise the object is merely checked for being instanceof P and the method m on the $this object is called directly.

Defining it this way preserves LSP guarantees, with maximum flexibility - making it for example perfectly possible to call array_map(ParentClass::someValue(this: ?), $objectsArray) without being surprised that it subtly actually calls the child method someValue on objects which are instanceof the containing class.

The neat benefit is that any $obj->method() call is now generalized (on the surface of the language semantics, obviously not implementation wise) as {get_class($obj)}::method(this: $obj), which makes the this-PFA a trivial extension of just having ? for the this parameter.
It also obsoletes any concerns about how the this parameter ends up called in the resulting PFA - because it's just a "normal" parameter then, from the perspective of the caller.

The only small caveat is Closure::__invoke(this: ?), which literally is just sort of an identity function and thus useless. To solve that, we should just decide to have an explicit this parameter on Closures take precendence. (Ultimately __invoke is more of an implementation detail of Closure, than anything else).

Also of note is that having a proper implicit $this parameter on methods must not have a position (as in positional parameters). Otherwise conflicts arise with e.g. the implicit $this forwarding in parent::m() syntax in class scope. (obviously, once you create a PFA P::m(this: ?, ?) the first positional parameter becomes the $this parameter and the original first parameter is now the second one.)

To summarize: Please introduce a first-class implicit $this parameter on any non-static method call, and have PFA work naturally with it, without doing contortions in language semantics / introducing a very PFA-specific syntax.

Thanks,
Bob

P.s.: You should possibly add to future scope allowing object::someMethod(this: ?) or class::someMethod(this: ?) to allow proper duck typing without having to know the actual object behind. That's something I see people asking for, too.

On Fri, Feb 6, 2026, at 4:05 PM, Bob Weinand wrote:

I'd like to search for appropriate words...

You were apparently unsuccessful.

On 6.2.2026 21:30:11, Larry Garfield wrote:

Sure, but we can fiddle with the details to find something that works. I also suggested something like these to Arnaud off-list:

(?: Test)->stuff(?);
(Test ?)->stuff(?);
((Test)?)->stuff(?);

--Larry Garfield

Could you please not push this garbage.

As stated repeatedly, this is just idiating on possible syntaxes that allow for an abbreviated form when the object is the only thing being partialed. If you don't like the ideas thrown out so far, please share your own rather than getting insulting.

My original plan was to use `$$->foo(3, 4)`, which would always produce a single-parameter closure that would invoke `foo(3, 4)` on whatever object was passed to it. I was not involved in this RFC from Arnaud and Tim, but I'm trying to look for ways to improve it so that it can cover the most common use case better.

I very much support the idea, but I don't think I would vote Yes on the current syntax. It's far too cumbersome for the most common case.

Assistance finding a syntax that would satisfy all presented situations would be more welcome than calling brainstorming "garbage."

--Larry Garfield