# First-class callable partial application

- Thread: https://externals.io/message/119678
- Original archive: https://news-web.php.net/php.internals/119678
- Started by Eugene Sidelnyk on 2023-03-10T19:04:26+00:00
- Messages: 26
- Page 1 of 2: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/119678.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #119678 — Eugene Sidelnyk — 2023-03-10T19:04:26+00:00

URL: https://externals.io/message/119678#119678

Hello internals!
I'm just wondering why First class callable syntax doesn't allow partial
application?
Recently I stumbled across following scenario where it could be really
useful:

```
    public function __invoke(SendOtpCommand $command)
    {
        $this->cache->get($command->getPhone(), $this->storeOtp($command,
...));
    }

    private function storeOtp(SendOtpCommand $command, ItemInterface $item)
    {
        // some logic
    }
```

In this example, I supposed that the closure created will accept a single
parameter $item, and when it is called back, method storeOtp will accept
both $command and $item. As it turned out to be, it doesn't really work
this way.

Another simplified example:

```
// partial application
foo(bar(1, ...));

function foo(Closure $closure)
{
    $closure(2);
}

function bar(int $a, int $b)
{
    var_dump($a, $b); // 1, 2
}
```

Closure in foo accepts only one parameter. But when it is actually
dispatched, bar is called with two arguments.

Are there any pitfalls, which prevent implementation of this nifty feature?

---

## #119680 — Larry Garfield — 2023-03-10T19:52:57+00:00

URL: https://externals.io/message/119678#119680 · In reply to: #119678

> Hello internals!
> I'm just wondering why First class callable syntax doesn't allow partial
> application?
> Recently I stumbled across following scenario where it could be really
> useful:
>
> ```
>     public function __invoke(SendOtpCommand $command)
>     {
>         $this->cache->get($command->getPhone(), $this->storeOtp($command,
> ...));
>     }
>
>     private function storeOtp(SendOtpCommand $command, ItemInterface $item)
>     {
>         // some logic
>     }
> ```
>
> In this example, I supposed that the closure created will accept a single
> parameter $item, and when it is called back, method storeOtp will accept
> both $command and $item. As it turned out to be, it doesn't really work
> this way.
>
> Another simplified example:
>
> ```
> // partial application
> foo(bar(1, ...));
>
> function foo(Closure $closure)
> {
>     $closure(2);
> }
>
> function bar(int $a, int $b)
> {
>     var_dump($a, $b); // 1, 2
> }
> ```
>
> Closure in foo accepts only one parameter. But when it is actually
> dispatched, bar is called with two arguments.
>
> Are there any pitfalls, which prevent implementation of this nifty feature?

There actually was an RFC for full partial application a few years ago:

https://wiki.php.net/rfc/partial_function_application

The main drawback is that in order to work, it had to do some very tricksy things with very critical parts of the code (how functions get called).  Enough people felt that was too risky for the functionality and it didn't pass.  First-class-callables were deliberately submitted (https://wiki.php.net/rfc/first_class_callable_syntax) as a sort of "junior version" of partial function application that could be done with far less invasive changes.

I'd *love* to see some form of PFA make a come-back, either with a simpler implementation or a reduced scope that allows for a simpler implementation.  I am not aware of anyone actively working on it at the moment, though.  (If anyone is, please speak up!)

--Larry Garfield

---

## #119681 — Eugene Sidelnyk — 2023-03-10T20:16:57+00:00

URL: https://externals.io/message/119678#119681 · In reply to: #119680

I glanced over this RFC and suggested syntax has question marks. Each one
stands for a single parameter.

Since there are named arguments already, it seems to be redundant. We'd
rather pass couple of named arguments and place the ... construction at the
ending than twirl around with question marks in order to describe which
parameters final closure will have.

When we do PFA, what we think of is what we pass, not what would be
necessary to pass afterwards.

And yes, any feature should start from the smallest possible form. If we
try to implement all at once, we would get nowhere.



> 
> > […]
> $item)
> > […]
> feature?
>
> There actually was an RFC for full partial application a few years ago:
>
> https://wiki.php.net/rfc/partial_function_application
>
> The main drawback is that in order to work, it had to do some very tricksy
> things with very critical parts of the code (how functions get called).
> Enough people felt that was too risky for the functionality and it didn't
> pass.  First-class-callables were deliberately submitted (
> https://wiki.php.net/rfc/first_class_callable_syntax) as a sort of
> "junior version" of partial function application that could be done with
> far less invasive changes.
>
> I'd *love* to see some form of PFA make a come-back, either with a simpler
> implementation or a reduced scope that allows for a simpler
> implementation.  I am not aware of anyone actively working on it at the
> moment, though.  (If anyone is, please speak up!)
>
> --Larry Garfield
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php

---

## #119682 — Larry Garfield — 2023-03-10T20:40:46+00:00

URL: https://externals.io/message/119678#119682 · In reply to: #119681

> I glanced over this RFC and suggested syntax has question marks. Each one
> stands for a single parameter.
>
> Since there are named arguments already, it seems to be redundant. We'd
> rather pass couple of named arguments and place the ... construction at the
> ending than twirl around with question marks in order to describe which
> parameters final closure will have.
>
> When we do PFA, what we think of is what we pass, not what would be
> necessary to pass afterwards.
>
> And yes, any feature should start from the smallest possible form. If we
> try to implement all at once, we would get nowhere.

(Please don't top-post.)

Named args were still very new at the time, so it wasn't clear the degree to which they should be required.  I still don't think only supporting named args is a good idea, as that creates additional work for common cases.  

Also, that wouldn't have changed the complexity part that people objected to.  It was the engine telling the difference at runtime between "foo($a, $b, $c)" and "foo($a, $b, ?)", which happens inside the C code for "call a function", that was the problem.  Named args wouldn't have mattered.

Sometimes features can start from small forms, but not always.  Sometimes that just boxes you in and makes it even harder to make the more complete version.  Case in point, `readonly` was the junior version of asymmetric visibility, but was implemented too quickly without thinking through all the implications, which then caused problems for actual asymmetric visibility's implementation.  That's one of the reasons asymmetric visibility didn't pass.  So in that case, the junior version prevented the full version from happening.

Anyway, until someone is able to commit time to coming up with a better implementation this is all moot. :-)

--Larry Garfield

---

## #119683 — Christian Schneider — 2023-03-10T21:55:52+00:00

URL: https://externals.io/message/119678#119683 · In reply to: #119678

Am 10.03.2023 um 20:04 schrieb Eugene Sidelnyk <zsidelnik@gmail.com>:
> Another simplified example:
> 
> ```
> // partial application
> foo(bar(1, ...));

While it is not as concise as your version I think the work-around with short arrow functions works ok unless your code is full of partial applications:
	foo(fn(...$a) =>  bar(1, ...$a));

Regards,
- Chris

---

## #119684 — Robert Landers — 2023-03-11T22:48:21+00:00

URL: https://externals.io/message/119678#119684 · In reply to: #119683

On Fri, Mar 10, 2023 at 10:56 PM Christian Schneider
<cschneid@cschneid.com> wrote:
>
> Am 10.03.2023 um 20:04 schrieb Eugene Sidelnyk <zsidelnik@gmail.com>:
> > […]
>
> While it is not as concise as your version I think the work-around with short arrow functions works ok unless your code is full of partial applications:
>         foo(fn(...$a) =>  bar(1, ...$a));
>
> Regards,
> - Chris
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>
Hey Eugene

I actually started on an implementation with this exact syntax this
past summer. I stopped after seeing the RFC was already declined years
ago. However, there are still some tricky parts to work out. Right
now, the "first-class callable" gets (basically) turned into a
callable string at the first possible instant, just after parsing.
This has to be thrown away, or handled in some other way. In my
implementation, I went with treating that as a special case, and
basically considering the partial application as 'syntax sugar'. There
are also other issues with the grammar, which I'll get to in a few
minutes.

My "syntax sugar" implementation was that this:

return $this->cache->get($command->getPhone(), $this->storeOtp($command, ...));

gets turned into this:

return fn(...$x) => $this-cache->get($command->getPhone(),
$this->storeOtp($command, ...$x));

There were a number of edge cases to solve, but I stopped
implementation before I got there.

The biggest issue was that the grammar became a little bit ambiguous.
I'm sure someone smarter than me can figure out how to define the
grammar in a way that isn't ambiguous... but the gist is that the
triple-dot is also used for array unpacking, thus I ended up disabling
that code path to get it to work.

I can probably clean up my code and submit a PR, if anyone would be interested.

Cheers,

Rob

---

## #119685 — Rowan Tommins — 2023-03-13T11:27:39+00:00

URL: https://externals.io/message/119678#119685 · In reply to: #119684

Hi all,

On Sat, 11 Mar 2023 at 22:48, Robert Landers <landers.robert@gmail.com>
wrote:

> My "syntax sugar" implementation was that this:
>
> return $this->cache->get($command->getPhone(), $this->storeOtp($command,
> ...));
>
> gets turned into this:
>
> return fn(...$x) => $this-cache->get($command->getPhone(),
> $this->storeOtp($command, ...$x));
>
> There were a number of edge cases to solve, but I stopped
> implementation before I got there.
>


As someone who voted for the RFC a couple of years ago, and was
disappointed it didn't pass, I'm pleased to see more interest in this.

It's worth noting that the previous implementation was reached after a lot
of different experiments, and there were a few requirements that guided it,
including (in no particular order, and I've probably forgotten something):

* Placeholders need to be possible for any parameter, not just
left-to-right; e.g. it must be possible to apply the callback parameter to
array_filter (which is the 2nd param), producing a callable that accepts an
array to filter
* It should not be possible to partially apply a non-existent function;
this is one of the big advantages of first-class callable syntax over
existing string-based callables: https://3v4l.org/Vr9oq#v8.2.3
* Errors from passing incorrect parameters should show as an error in the
actual call, not the wrapper; compare existing FCC behaviour with a manual
lambda: https://3v4l.org/Q2H2Z#v8.2.3
* The generated callable should have correct parameter type and name
information visible in reflection (closely related to previous point)
* The relationship with named parameters and variadics needs to at least be
well-defined, even if it's "error with a possibility of expanding
functionality later" (this caused a lot of pain in some of the
implementation ideas)
* Repeated partial application (currying and similar) should ideally not
result in a whole stack of wrappers; that is $f = foo(42, ?, ?); $g =
$f(69, ?); should result in the same object as $g = foo(42, 69, ?);

This is definitely one of those features where "the devil is in the
details", and a simpler implementation is possible, but may not be
desirable.

Regards,
-- 
Rowan Tommins
[IMSoP]

---

## #119687 — Robert Landers — 2023-03-13T18:44:48+00:00

URL: https://externals.io/message/119678#119687 · In reply to: #119685

>
> Hi all,
>
> On Sat, 11 Mar 2023 at 22:48, Robert Landers <landers.robert@gmail.com>
> wrote:
>
> > […]
>
>
> As someone who voted for the RFC a couple of years ago, and was
> disappointed it didn't pass, I'm pleased to see more interest in this.
>
> It's worth noting that the previous implementation was reached after a lot
> of different experiments, and there were a few requirements that guided it,
> including (in no particular order, and I've probably forgotten something):
>
> * Placeholders need to be possible for any parameter, not just
> left-to-right; e.g. it must be possible to apply the callback parameter to
> array_filter (which is the 2nd param), producing a callable that accepts an
> array to filter
> * It should not be possible to partially apply a non-existent function;
> this is one of the big advantages of first-class callable syntax over
> existing string-based callables: https://3v4l.org/Vr9oq#v8.2.3
> * Errors from passing incorrect parameters should show as an error in the
> actual call, not the wrapper; compare existing FCC behaviour with a manual
> lambda: https://3v4l.org/Q2H2Z#v8.2.3
> * The generated callable should have correct parameter type and name
> information visible in reflection (closely related to previous point)
> * The relationship with named parameters and variadics needs to at least be
> well-defined, even if it's "error with a possibility of expanding
> functionality later" (this caused a lot of pain in some of the
> implementation ideas)
> * Repeated partial application (currying and similar) should ideally not
> result in a whole stack of wrappers; that is $f = foo(42, ?, ?); $g =
> $f(69, ?); should result in the same object as $g = foo(42, 69, ?);
>
> This is definitely one of those features where "the devil is in the
> details", and a simpler implementation is possible, but may not be
> desirable.
>
> Regards,
> --
> Rowan Tommins
> [IMSoP]

Hey Rowan,

> This is definitely one of those features where "the devil is in the
> details", and a simpler implementation is possible, but may not be
> desirable.

My approach was more of an iterative one.

1. Get left-right done so that

$x = something($left, $right, ...);

would be allowed, but not

$x = something($left, ..., $right);

This would bring some immediate benefits, as initially proposed. I'd
also propose that variadic arguments wouldn't be allowed, thus

array_map(..., $x);

would be allowed, but not

array_map($callback, ...);

2. Implement middle/variadic arguments

I felt that this could be it's own RFC once the above is implemented.
As you said, the devil is in the details... what does

array_map($callback, ..., $right)

do? Is the partial application variadic? These are things that I felt
couldn't realistically be answered until we saw some real-life usage
and feedback from (1) above.

3. Optimizations

This would be when we flatten the callback chain OR maybe by now, the
genius(es) working on the opcache would have already beaten everyone
to the punch.

---

## #119688 — Rowan Tommins — 2023-03-13T20:36:59+00:00

URL: https://externals.io/message/119678#119688 · In reply to: #119687

>My approach was more of an iterative one.
>
>1. Get left-right done so that
>
>$x = something($left, $right, ...);
>
>would be allowed, but not
>
>$x = something($left, ..., $right);
>
>This would bring some immediate benefits, as initially proposed.


There are three problems I see with that:

1) The choice of syntax for the first version immediately limits the options for later additions, so you still need to discuss them. (IIRC, the proposal that went to vote had separate ? and ... tokens, but a number of different variations were experimented with.)

2) Most of the goals I mentioned above are not about the syntax, and are no easier for left-to-right - for instance, copying the type information for use in reflection and error messages.

3) Unlike purely or primarily functional languages, PHP's standard library is not at all designed with parameter orders that lend themselves to left-to-right application. I gave array_filter as an example, because it's exactly the kind of function people will want to use PFA for, but wouldn't be able to with left-to-right only.

I don't want to put you off exploring this idea, though, so feel free to take this all with as many pinches of salt as you want.

Regards,

-- 
Rowan Tommins
[IMSoP]

---

## #119689 — Larry Garfield — 2023-03-13T20:44:10+00:00

URL: https://externals.io/message/119678#119689 · In reply to: #119688

> 
> > […]
>
>
> There are three problems I see with that:
>
> 1) The choice of syntax for the first version immediately limits the 
> options for later additions, so you still need to discuss them. (IIRC, 
> the proposal that went to vote had separate ? and ... tokens, but a 
> number of different variations were experimented with.)
>
> 2) Most of the goals I mentioned above are not about the syntax, and 
> are no easier for left-to-right - for instance, copying the type 
> information for use in reflection and error messages.
>
> 3) Unlike purely or primarily functional languages, PHP's standard 
> library is not at all designed with parameter orders that lend 
> themselves to left-to-right application. I gave array_filter as an 
> example, because it's exactly the kind of function people will want to 
> use PFA for, but wouldn't be able to with left-to-right only.

Personally, I consider this the easiest thing to "let go of."  As has been discussed numerous times, all of the most used array functions need to be redesigned to work with iterables, and in many cases make more sense.  That would be a natural time to also revisit parameter order to fit with whatever partial application syntax was in use.

There are limitations to that (a left-to-right-only approach would then preclude optional arguments, which is not ideal), but "compatibility with the array functions written in 1996" is probably my least cared-about criteria.

That said, full support as the original RFC had would be ideal.  It's doing it in a clean and performant fashion that is the challenge.

--Larry Garfield

---

## #119690 — Rowan Tommins — 2023-03-14T09:16:24+00:00

URL: https://externals.io/message/119678#119690 · In reply to: #119689

> As has been discussed numerous times, all of the most used array functions need to be redesigned to work with iterables, and in many cases make more sense.  That would be a natural time to also revisit parameter order to fit with whatever partial application syntax was in use.


It's not just the array functions, though, it's every single function 
built into PHP, and an even longer list of userland library and 
framework functions; and there will always be competing reasons for 
preferring one signature over another. What attracts me about features 
like PFA is precisely that they let you work in new ways *without* 
having to rewrite all of that.

Some more examples of placeholder-first application, from a quick skim 
through the documentation:

$escape = htmlspecialchars(?, ENT_XML1);
$containsAt = str_contains(?, '@');
$priceFormatter = number_format(?, 2, ',', '.');
$addSigToFile = file_put_contents(?, $signature, FILE_APPEND);
$takeOwnership = chown(?, `get_current_user()`);
$encode = json_encode(?, `JSON_THROW_ON_ERROR` | JSON_PRESERVE_ZERO_FRACTION);
$unserialize = unserialize(?, ['allowed_classes' => false]);
$isLogger = is_subclass_of(?, LoggerInterface::class, false);

I'm sure I could look through Laravel's documentation, or Symfony's, and 
find examples there too.

Regards,

-- 
Rowan Tommins
[IMSoP]

---

## #119691 — Bob Weinand — 2023-03-14T10:39:44+00:00

URL: https://externals.io/message/119678#119691 · In reply to: #119690

> Am 14.03.2023 um 10:16 schrieb Rowan Tommins <rowan.collins@gmail.com>:
> 
> ﻿On 13/03/2023 20:44, Larry Garfield wrote:
> > […]
> 
> 
> It's not just the array functions, though, it's every single function built into PHP, and an even longer list of userland library and framework functions; and there will always be competing reasons for preferring one signature over another. What attracts me about features like PFA is precisely that they let you work in new ways *without* having to rewrite all of that.
> 
> Some more examples of placeholder-first application, from a quick skim through the documentation:
> 
> $escape = htmlspecialchars(?, ENT_XML1);
> $containsAt = str_contains(?, '@');
> $priceFormatter = number_format(?, 2, ',', '.');
> $addSigToFile = file_put_contents(?, $signature, FILE_APPEND);
> $takeOwnership = chown(?, `get_current_user()`);
> $encode = json_encode(?, `JSON_THROW_ON_ERROR` | JSON_PRESERVE_ZERO_FRACTION);
> $unserialize = unserialize(?, ['allowed_classes' => false]);
> $isLogger = is_subclass_of(?, LoggerInterface::class, false);
> 
> I'm sure I could look through Laravel's documentation, or Symfony's, and find examples there too.
> 
> Regards,
> 
> -- 
> Rowan Tommins
> [IMSoP]

Hey Rowan,

do we actually need *positional* partial application, after a ... token?

Would it not be enough, to simply forbid positional arguments after a ... and just allow named arguments? These already have well defined position independent semantics.

There may be some desire for a single argument placeholder later on, but this can be introduced later, separately.

Bob

---

## #119692 — Rowan Tommins — 2023-03-14T12:56:50+00:00

URL: https://externals.io/message/119678#119692 · In reply to: #119691

> Hey Rowan,
>
> do we actually need *positional* partial application, after a ... token?
>
> Would it not be enough, to simply forbid positional arguments after a ...
> and just allow named arguments? These already have well defined position
> independent semantics.
>
> There may be some desire for a single argument placeholder later on, but
> this can be introduced later, separately.
>


Yes, named parameters would certainly be better than left-to-right only.
It's definitely less elegant, though, and given that PFA is largely
short-hand for a short closure anyway, I think conciseness is quite an
important aim.

To take a couple of the above examples, and compare existing short closure,
fully positional PFA, and named-after-placeholder PFA:

$isLogger = fn($object) => is_subclass_of($object, LoggerInterface::class,
false);
$isLogger = is_subclass_of(?, LoggerInterface::class, false);
$isLogger = is_subclass_of(..., class: LoggerInterface::class,
allow_string: false);

$priceFormatter = fn(float $num) => number_format($num, 2, ',', '.');
$priceFormatter = number_format(?, 2, ',', '.');
$priceFormatter = number_format(..., decimals: 2, decimal_separator: ',',
thousands_separator: '.');

Arguably the named param version is more explicit, but in some cases it's
significantly longer than manually defining a closure, whereas fully
positional PFA is always shorter.

Regards,
-- 
Rowan Tommins
[IMSoP]

---

## #119693 — Robert Landers — 2023-03-14T13:50:40+00:00

URL: https://externals.io/message/119678#119693 · In reply to: #119692

>
> 
>
> > […]
>
>
> Yes, named parameters would certainly be better than left-to-right only.
> It's definitely less elegant, though, and given that PFA is largely
> short-hand for a short closure anyway, I think conciseness is quite an
> important aim.
>
> To take a couple of the above examples, and compare existing short closure,
> fully positional PFA, and named-after-placeholder PFA:
>
> $isLogger = fn($object) => is_subclass_of($object, LoggerInterface::class,
> false);
> $isLogger = is_subclass_of(?, LoggerInterface::class, false);
> $isLogger = is_subclass_of(..., class: LoggerInterface::class,
> allow_string: false);
>
> $priceFormatter = fn(float $num) => number_format($num, 2, ',', '.');
> $priceFormatter = number_format(?, 2, ',', '.');
> $priceFormatter = number_format(..., decimals: 2, decimal_separator: ',',
> thousands_separator: '.');
>
> Arguably the named param version is more explicit, but in some cases it's
> significantly longer than manually defining a closure, whereas fully
> positional PFA is always shorter.
>
> Regards,
> --
> Rowan Tommins
> [IMSoP]

Something I was partial to (pun slightly intended), when thinking
about it last summer was to put named parameters with dots following,
like this:

$isLogger = is_subclass_of(object_or_class..., LoggerInterface::class, false);
// or, since this is a beginning/end partial, this is the same:
$isLogger = is_subclass_of(..., LoggerInterface::class, false);

$isLogger(object_or_class: $myClass);
// or
$isLogger($myClass);

$priceFormatter = number_format(num..., 2, ',', '.');

At least, that was what I was going to propose, according to my notes.
Looking at it 8 months later, I still kinda like it.

---

## #119694 — Larry Garfield — 2023-03-14T16:57:57+00:00

URL: https://externals.io/message/119678#119694 · In reply to: #119693

> 
> > […]
>
> Something I was partial to (pun slightly intended), when thinking
> about it last summer was to put named parameters with dots following,
> like this:
>
> $isLogger = is_subclass_of(object_or_class..., LoggerInterface::class, false);
> // or, since this is a beginning/end partial, this is the same:
> $isLogger = is_subclass_of(..., LoggerInterface::class, false);
>
> $isLogger(object_or_class: $myClass);
> // or
> $isLogger($myClass);
>
> $priceFormatter = number_format(num..., 2, ',', '.');
>
> At least, that was what I was going to propose, according to my notes.
> Looking at it 8 months later, I still kinda like it.

For reference, in the original RFC we had the following rules:

```
This RFC introduces two place holder symbols:

    The argument place holder ? means that exactly one argument is expected at this position.
    The variadic place holder ... means that zero or more arguments may be supplied at this position.

The following rules apply to partial application:

    ... may only occur zero or one time
    ... may only be followed by named arguments
    named arguments must come after all place holders
    named placeholders are not supported
```

The reason for that was, I believe, because it was too complicated figure out the signature of the resulting closure if named placeholders were allowed after the `...` .  Optional arguments also got weird, IIRC.  We went around *a lot* on the details here before settling on the syntax we did.

I agree that requiring all partials to used named args would be a big step down for usability.

But again, none of this addresses the root issue that doomed the previous RFC: Any syntax that involves a runtime determination of "is this a function call or a partial application?" is going to involve some really tricky dancing along the critical path of all function calls.  Even though Joe's implementation had no significant performance impact (AFAIR), it still made the code more complex and potentially harder to optimize in the future.

So the only ways forward would be:

1) Find another approach in the implementation that doesn't have that problem, which may then changes to the proposed syntax and possibility capabilities.  (I had suggested some prefix on the call to indicate that it's a partial and not a call, like %foo(1, ?, 3, ...) or something.)
2) The voter base shifts and/or changes its mind and decides that the extra complexity is worth it.

Redoing the syntax bikeshedding from 3 years ago (dear god, it's been 3 years?) before one of those two is answered is not productive.  New engine approach first, then syntax based on what that approach allows.

--Larry Garfield

---

## #119707 — Robert Landers — 2023-03-15T10:17:04+00:00

URL: https://externals.io/message/119678#119707 · In reply to: #119694

>
> 
> > […]
>
> For reference, in the original RFC we had the following rules:
>
> ```
> This RFC introduces two place holder symbols:
>
>     The argument place holder ? means that exactly one argument is expected at this position.
>     The variadic place holder ... means that zero or more arguments may be supplied at this position.
>
> The following rules apply to partial application:
>
>     ... may only occur zero or one time
>     ... may only be followed by named arguments
>     named arguments must come after all place holders
>     named placeholders are not supported
> ```
>
> The reason for that was, I believe, because it was too complicated figure out the signature of the resulting closure if named placeholders were allowed after the `...` .  Optional arguments also got weird, IIRC.  We went around *a lot* on the details here before settling on the syntax we did.
>
> I agree that requiring all partials to used named args would be a big step down for usability.
>
> But again, none of this addresses the root issue that doomed the previous RFC: Any syntax that involves a runtime determination of "is this a function call or a partial application?" is going to involve some really tricky dancing along the critical path of all function calls.  Even though Joe's implementation had no significant performance impact (AFAIR), it still made the code more complex and potentially harder to optimize in the future.
>
> So the only ways forward would be:
>
> 1) Find another approach in the implementation that doesn't have that problem, which may then changes to the proposed syntax and possibility capabilities.  (I had suggested some prefix on the call to indicate that it's a partial and not a call, like %foo(1, ?, 3, ...) or something.)
> 2) The voter base shifts and/or changes its mind and decides that the extra complexity is worth it.
>
> Redoing the syntax bikeshedding from 3 years ago (dear god, it's been 3 years?) before one of those two is answered is not productive.  New engine approach first, then syntax based on what that approach allows.
>
> --Larry Garfield
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>

Hey Larry,

> Redoing the syntax bikeshedding from 3 years ago (dear god, it's been 3 years?) before one of those two is answered is not productive.  New engine approach first, then syntax based on what that approach allows.

This is why my approach was one of syntax sugar. Sure, it would make
the call stack weird during exceptions, but all-in-all, not much
changes from an engine standpoint. Most of the changes were to the
grammar IIRC. I'll hop on my old computer this weekend and submit a PR
to make the discussion easier.

---

## #119716 — Eugene Sidelnyk — 2023-03-15T20:32:08+00:00

URL: https://externals.io/message/119678#119716 · In reply to: #119691

>
> > […]
> functions need to be redesigned to work with iterables, and in many cases
> make more sense.  That would be a natural time to also revisit parameter
> order to fit with whatever partial application syntax was in use.
> > […]
> built into PHP, and an even longer list of userland library and framework
> functions; and there will always be competing reasons for preferring one
> signature over another. What attracts me about features like PFA is
> precisely that they let you work in new ways *without* having to rewrite
> all of that.
> > […]
> through the documentation:
> > […]
> JSON_PRESERVE_ZERO_FRACTION);
> > […]
> find examples there too.
> > […]
>
> Hey Rowan,
>
> do we actually need *positional* partial application, after a ... token?
>
> Would it not be enough, to simply forbid positional arguments after a ...
> and just allow named arguments? These already have well defined position
> independent semantics.
>
> There may be some desire for a single argument placeholder later on, but
> this can be introduced later, separately.
>
> Bob


Personally, I agree with Robert. It seems logical to have ... only at the
end of arguments list. Anything else can be bound by positional and named
arguments.

For instance:
```
str_contains(?, '@'); // do you remember if arg 2 is a needle or a haystack?
str_contains(needle: '@', ...); // far more readable when explicitly
specified

explode(?, $str) // do you remember that argument 2 is actual string being
exploded (not a delimiter)?
explode(string: $str, ...) // delimiter will be provided to the closure
```

On the first stage, maybe it's better to have only positional arguments
available without a possibility to provide named ones.
I guess this will reveal a lot of pitfalls to consider.  For instance, if
some third-party library function signature has a signature with a variadic
parameter `foo($bar, $baz, ...$x)`. We make a closure for it like this
`foo(baz: $b, ...)` and it works fine. Later on authors of a library rename
$baz into $tar and it gets unnoticed. Hence, the new signature is
`foo($bar, $tar, ...$x)`. How would `foo(baz: $b, ...)` work? Will it put
['baz' => $b] into an $x argument?

Firstly, it would make sense to only allow simple use cases of PFA.  E.g.
only positional parameters. Hence, it won't be possible to use PFA if we
want to bind non-prefix parameters. Though, well-designed functions, which
declare parameters starting from the most generic unto most specific, will
perfectly fit into PFA.

---

## #119717 — Dan Ackroyd — 2023-03-15T21:12:11+00:00

URL: https://externals.io/message/119678#119717 · In reply to: #119694

>
> New engine approach first, then syntax based on what that approach allows.

Would it be desirable to split those two things into two separate
RFCs, by having the first RFC not have native syntax support, but
instead another static method on Closure? e.g. something like:

Closure::partial($callable, array $position_params, array
$named_params): Closure {}

and would follow the pattern of Closure::fromCallable() being
implemented in 7.1, and the built-in syntax took until 8.1.

That would also allow creating partially applied functions in a data-driven way.

cheers
Dan
Ack

---

## #119718 — Rowan Tommins — 2023-03-16T09:14:38+00:00

URL: https://externals.io/message/119678#119718 · In reply to: #119717

> Would it be desirable to split those two things into two separate
> RFCs, by having the first RFC not have native syntax support, but
> instead another static method on Closure? e.g. something like:
>
> Closure::partial($callable, array $position_params, array
> $named_params): Closure {}


Hm... now we have the first-class callable syntax, making it an instance 
method on Closure would allow this:

$mapFoo = array_map(...)->partial([$foo]);
$filterFoo = array_filter(...)->partial([1 => $foo]);

Which could copy over the full signature, so be equivalent to this:

$mapFoo = static fn(array ...$arrays): array => array_map($foo, ...$arrays);
$filterFoo = static fn(array $array, int $mode = 0): array => 
array_filter($array, $foo, $mode);

While being a similar length to a much less rich version:

$mapFoo = fn($array) => array_map($foo, $array);
$filterFoo = fn($array) => array_filter($array, $foo);


Regards,

-- 
Rowan Tommins
[IMSoP]

---

## #119721 — Robert Landers — 2023-03-16T10:44:56+00:00

URL: https://externals.io/message/119678#119721 · In reply to: #119718

>
> 
> > […]
>
>
> Hm... now we have the first-class callable syntax, making it an instance
> method on Closure would allow this:
>
> $mapFoo = array_map(...)->partial([$foo]);
> $filterFoo = array_filter(...)->partial([1 => $foo]);
>
> Which could copy over the full signature, so be equivalent to this:
>
> $mapFoo = static fn(array ...$arrays): array => array_map($foo, ...$arrays);
> $filterFoo = static fn(array $array, int $mode = 0): array =>
> array_filter($array, $foo, $mode);
>
> While being a similar length to a much less rich version:
>
> $mapFoo = fn($array) => array_map($foo, $array);
> $filterFoo = fn($array) => array_filter($array, $foo);
>
>
> Regards,
>
> --
> Rowan Tommins
> [IMSoP]
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>

Rowan, that is actually fairly beautiful.

May not even need the second RFC...

---

## #119731 — Larry Garfield — 2023-03-16T22:26:00+00:00

URL: https://externals.io/message/119678#119731 · In reply to: #119718

> 
> > […]
>
>
> Hm... now we have the first-class callable syntax, making it an instance 
> method on Closure would allow this:
>
> $mapFoo = array_map(...)->partial([$foo]);
> $filterFoo = array_filter(...)->partial([1 => $foo]);
>
> Which could copy over the full signature, so be equivalent to this:
>
> $mapFoo = static fn(array ...$arrays): array => array_map($foo, ...$arrays);
> $filterFoo = static fn(array $array, int $mode = 0): array => 
> array_filter($array, $foo, $mode);
>
> While being a similar length to a much less rich version:
>
> $mapFoo = fn($array) => array_map($foo, $array);
> $filterFoo = fn($array) => array_filter($array, $foo);

Fascinating!  I... don't know if we considered something like that or not 3 years ago.  It's been a while.

It's definitely not as nice as the integrated syntax, but it does have the advantage of the implementation almost certainly being rather pedestrian in comparison.  That approach would favor left-to-right application, but not force it, which is probably sufficient.

As a thought experiment, if we had that syntax and functions that were designed to be used with them, it would look like so:

function amap(callable $c, iterable $it) { ... }
function implode(string $sep, iterable $it) { ... }
function length(string $s) { ... }

$arr = [1, 2, 3];

$a2 = amap(...)->partial(chr(...))($arr);

$str = implode(...)->partial(',')($a2);

Or, if combined with pipes:

$size = $arr 
|> amap(...)->partial(chr(...))
|> implode(...)->partial(',')
|> length(...);

Which... is not terrible, especially as it doesn't preclude using higher order functions for more control.  

We would likely want partial() to accept positional args (left to right) and named args.  And I'm more than happy to say now that the capture is only by value, ever.

I wonder if there's a shorter name we could use than "partial" to make it more readable?  Or perhaps an operator that applied only to closure objects?  

--Larry Garfield

---

## #119734 — michal.brzuchalski@gmail.com — 2023-03-17T08:54:20+00:00

URL: https://externals.io/message/119678#119734 · In reply to: #119731

Hi Larry,

czw., 16 mar 2023 o 23:26 Larry Garfield <larry@garfieldtech.com>
napisał(a):

> 
> > […]
> ...$arrays);
> > […]
>
> Fascinating!  I... don't know if we considered something like that or not
> 3 years ago.  It's been a while.
>
> It's definitely not as nice as the integrated syntax, but it does have the
> advantage of the implementation almost certainly being rather pedestrian in
> comparison.  That approach would favor left-to-right application, but not
> force it, which is probably sufficient.
>
> As a thought experiment, if we had that syntax and functions that were
> designed to be used with them, it would look like so:
>
> function amap(callable $c, iterable $it) { ... }
> function implode(string $sep, iterable $it) { ... }
> function length(string $s) { ... }
>
> $arr = [1, 2, 3];
>
> $a2 = amap(...)->partial(chr(...))($arr);
>
> $str = implode(...)->partial(',')($a2);
>
> Or, if combined with pipes:
>
> $size = $arr
> |> amap(...)->partial(chr(...))
> |> implode(...)->partial(',')
> |> length(...);
>
> Which... is not terrible, especially as it doesn't preclude using higher
> order functions for more control.
>

Maybe we could introduce two additional methods on a Closure similar to
what Java have
https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html
* andThen() - which functionality is like a pipe operator
* apply() - which you can call without the option to bind/rebind and just
pass arguments for execution

The pipe operator can be introduced later, but we could already have the
functionality on Closure.

The above example might look readable as well:

$size = amap(...)->partial(chr(...))
    ->andThen(implode(...)->partial(','))
    ->andThen(length(...))
    ->apply($arr);

Cheers,
Michał Marcin Brzuchalski

---

## #119735 — Robert Landers — 2023-03-17T09:52:37+00:00

URL: https://externals.io/message/119678#119735 · In reply to: #119731

>
> 
> > […]
>
> Fascinating!  I... don't know if we considered something like that or not 3 years ago.  It's been a while.
>
> It's definitely not as nice as the integrated syntax, but it does have the advantage of the implementation almost certainly being rather pedestrian in comparison.  That approach would favor left-to-right application, but not force it, which is probably sufficient.
>
> As a thought experiment, if we had that syntax and functions that were designed to be used with them, it would look like so:
>
> function amap(callable $c, iterable $it) { ... }
> function implode(string $sep, iterable $it) { ... }
> function length(string $s) { ... }
>
> $arr = [1, 2, 3];
>
> $a2 = amap(...)->partial(chr(...))($arr);
>
> $str = implode(...)->partial(',')($a2);
>
> Or, if combined with pipes:
>
> $size = $arr
> |> amap(...)->partial(chr(...))
> |> implode(...)->partial(',')
> |> length(...);
>
> Which... is not terrible, especially as it doesn't preclude using higher order functions for more control.
>
> We would likely want partial() to accept positional args (left to right) and named args.  And I'm more than happy to say now that the capture is only by value, ever.
>
> I wonder if there's a shorter name we could use than "partial" to make it more readable?  Or perhaps an operator that applied only to closure objects?
>
> --Larry Garfield
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>

Hey Larry,

> We would likely want partial() to accept positional args (left to right) and named args.

I think this is already supported-ish. For example, here's a partial
application in user space that seems to follow all the rules. Or at
least the rules that are followed make sense.

function partial(Closure $callable, ...$args): Closure {
   return static fn(...$fullArgs) => $callable(...[...$args, ...$fullArgs]);
}

https://onlinephp.io/c/9195a

It only needs a bit of edge-condition handling (like when a parameter
is filled, you probably shouldn't be able to override arbitrary
parameters).

---

## #119736 — Rowan Tommins — 2023-03-17T10:35:44+00:00

URL: https://externals.io/message/119678#119736 · In reply to: #119735

On Fri, 17 Mar 2023 at 09:52, Robert Landers <landers.robert@gmail.com>
wrote:

>
> I think this is already supported-ish. For example, here's a partial
> application in user space that seems to follow all the rules. Or at
> least the rules that are followed make sense.
>
> function partial(Closure $callable, ...$args): Closure {
>    return static fn(...$fullArgs) => $callable(...[...$args,
> ...$fullArgs]);
> }
>


The big difference between this and a native implementation is that the
closure doesn't have any information in its type signature. As well as
leading to less helpful error messages (type errors will be reported as
happening _inside_ the closure, rather than on the line that calls it),
this makes the parameters invisible to Reflection.

There's also no way to make it an instance method from userland, making
everything a bit verbose - Michał's example would become something like:

$size = Closure::apply(
     Closure::pipe(
          Closure::partial(amap(...), chr(...)),
          Closure::partial( implode (...), ','),
          amap(...)
     ),
    $arr
);

Regards,
-- 
Rowan Tommins
[IMSoP]

---

## #119740 — Larry Garfield — 2023-03-17T14:45:18+00:00

URL: https://externals.io/message/119678#119740 · In reply to: #119734

> > […]
>
> Maybe we could introduce two additional methods on a Closure similar to
> what Java have
> https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html
> * andThen() - which functionality is like a pipe operator
> * apply() - which you can call without the option to bind/rebind and just
> pass arguments for execution
>
> The pipe operator can be introduced later, but we could already have the
> functionality on Closure.
>
> The above example might look readable as well:
>
> $size = amap(...)->partial(chr(...))
>     ->andThen(implode(...)->partial(','))
>     ->andThen(length(...))
>     ->apply($arr);

See that brings up a subtle difference between two operations: pipe and compose.

Compose takes two functions A and B and returns a new function that is logically identical to B(A()).  (Or sometimes A(B()), depending on the language, which is all kinds of confusing.)

Pipe takes an arbitrary value and unary function and calls the function with that value immediately, returning the result.

You can build a pipe equivalent out of compose, although it's a bit clunky and the semantics are not *quite* identical.  (The order of execution is different, which may or may not matter depending on the specifics of each call.)

In the example above, andThen() is acting as a compose operator, while apply() is just boring function application.  It works but it's a bit clunky compared to a proper pipe.  That said, there are user space libraries that do that.

Thinking aloud...  I said before it would be better to have a native operator for all of these.  (compose, pipe, and partial.)  If we restrict ourselves to Closure objects rather than all callables (since callables are a lot messier), that does open up some new options.  Specifically, the following are already syntax errors today:

$c = strlen(...);
$d = array_map(...);

$c + $d; // not allowed.
$c . $d; // not allowed, thinks it's string concat and $c isn't stringable.
$c[2, 3]; // not allowed.
$c{5}; // not allowed, thinks it's a string offset.

So that suggests to me the following:

* Define $a + $b on closure objects to be a compose operator that returns a new function equivalent to $b($a( )).  It would only work on unary functions, validate compatible types between the param and return values, and have the correct type information.

* Define $c{ ... } as a "partial application" call.  The {} body would be similar to a function call now; or maybe the original PFA syntax with ? and ... ?  Debatable, but the {} would be a better signal to the engine that we're not calling a function, just partially calling.  It's also reasonably self-evident to developers.

Those two together would allow for this:

(amap{chr(...), ?} + implode{?, separator: ','} + length(...))($arr);

Which is... pretty nice, really.  It's very close to the original PFA syntax and capabilities, compact, not easily confused with anything else, type safe, and the engine can handle optimizing around not having unnecessary interstitial functions internally.

I still think we should also add a pipe operator |> as well, but that would be just as compatible with the {}-based PFA syntax.  It's also fairly trivial to implement.

$size = $arr
  |> amap{chr(...), ?)
  |> implode(',', ?)
  |> length(...);

Thoughts?

--Larry Garfield

---

Next page: https://externals.io/message/119678.md?page=2
