A reader sent me a diff last week with one line circled in red: `$tag = sprintf('[%s] %s', currentRequestId(), ?);`. Her question: "Is this the same as the arrow function it replaced?" It isn't, and that one line tells you most of what you need to know about Partial Function Application in PHP 8.6. I like the feature a lot and I want it in our codebases on day one. I also think most of us are going to read it wrong for the first six months, because it looks like sugar for `fn` and behaves like a constructor that takes a snapshot.

Quick recap for anyone who skipped the RFC threads. PHP 8.6 is in beta, with the release planned for around November 19, 2026. PFA builds on the `trim(...)` first-class callable syntax we got in 8.1. You call a function or method normally, but you put a `?` in any slot you want to fill later, and you get a callable back. A single `?` stands for exactly one argument. A trailing `...` means every remaining argument, and it has to come last, after the positional slots and the named ones. If you write `str_replace(' ', '-', ?)` you have a slugifier with no closure, no parameter name you had to invent, and no `$value` repeated twice. It works on static and instance methods too, and it plays nicely with named arguments, so `Book::create(category: Category::THRILLER, title: ?)` reads almost like configuration.

This is the missing half of the pipe operator. Since `|>` arrived, every pipeline I wrote had one ugly link, the parenthesised arrow function you need the moment a step takes more than one argument. Five neat stages and then one `(fn ($v) => str_replace(' ', '-', $v))` sitting in the middle like a cable tie. With PFA that step becomes `str_replace(' ', '-', ?)` and the `?` shows exactly where the value flows in. Go people like to tell us that explicit loops beat clever chaining, and they have a point. But once a language has a pipe, the pipe needs this, and 8.6 delivers it.

Now back to that red circle. A partial evaluates every non-placeholder argument at the moment you create it. The value gets captured and reused on every call. So `currentRequestId()` in my reader's line ran exactly once, when the worker booted and built its formatter. The arrow function it replaced called that function on every log line. In a classic FPM request nobody notices, because the partial and the request die together. In a long-running Swoole, RoadRunner or queue worker, every log line for the next forty thousand jobs is stamped with the first job's ID. Your on-call engineer finds out at 2am, grepping for a request that seems to have handled the entire night's traffic.

The strongest objection deserves to be taken seriously. You could say this proves PFA is a footgun, that `?` is one more sigil in a language with plenty already, and that an explicit closure is more honest because it shows you exactly what runs when. I half agree. A closure makes the timing obvious and a partial hides it. But eager capture is the correct semantic, and it is often what you actually want: build the expensive thing once, call it many times. The closure version that re-runs `getPrefix()` on every call has its own bugs, they just look different. What I take from the objection is that we need a review habit. Nobody needs to ban the syntax.

The second trap is quieter. A partial is a brand new closure. It does not just point at the original function the way `strlen(...)` does. Custom attributes on the original are not carried over. The exceptions are `#[\NoDiscard]` and `#[\SensitiveParameter]`, and the latter only survives on parameters you left open. If your framework reflects on callables to find route metadata, validation rules or event-listener markers, a partial will slip past it silently. Teams with attribute-driven container wiring should grep for that before they let anyone hand partials to the registry.

So here is the house rule I am proposing for my own team. A `?` or a trailing `...` is fine anywhere the fixed arguments are literals, constants, enum cases or values already sitting in local variables. As soon as a fixed argument is a function call, a method call, or anything that reads the clock, the request or the container, the author either hoists it into a named variable on the line above, so the capture is visible, or writes a closure on purpose. Partials never go anywhere that Reflection will read attributes. It costs us one extra line now and then, and in exchange nobody has to guess when an argument gets evaluated.

I am not sure that rule is right, though. It might be too cautious for pipelines, where nearly every fixed argument is a literal anyway. It might be too loose for long-running workers. So tell me in the comments: when 8.6 ships in November, will your team allow function calls as fixed arguments in a partial, or will you require that captured values be hoisted into a variable first?