Re: First-class callable partial application
| From: | Larry Garfield | Date: | Fri, 17 Mar 2023 14:45:18 +0000 |
| Subject: | Re: First-class callable partial application | ||
| References: | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-119740@lists.php.net to get a copy of this message | ||
On Fri, Mar 17, 2023, at 3:54 AM, Michał Marcin Brzuchalski wrote:
>> 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);
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