Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Auto-currying #21

Closed
fabschurt opened this issue Aug 1, 2019 · 4 comments
Closed

[RFC] Auto-currying #21

fabschurt opened this issue Aug 1, 2019 · 4 comments

Comments

@fabschurt
Copy link

fabschurt commented Aug 1, 2019

Hi 👋

First of all: THANK YOU for this awesome library, I can now make use of some of my FP and Ramda knowledge in PHP 👍

One thing I miss from Ramda though is the default currying of the lib functions. Would you be open to seeing this feature implemented? I’m personally willing to pour some work into it if deemed relevant.

For example, without default currying:

<?php

private function calculateAverageRating(): float
{
    if (!count($this->ratings)) {
        return 0.0;
    }
​
    return pipe(
        $this->ratings,
        partial(map, methodCaller('getScore')),
        partial(reduce, sum),
        rpartial(div, count($this->ratings))
    );
}

…and with default currying:

<?php

private function calculateAverageRating(): float
{
    if (!count($this->ratings)) {
        return 0.0;
    }
​
    return pipe(
        $this->ratings,
        map(methodCaller('getScore')),
        reduce(sum),
        rpartial(div, count($this->ratings)) // here the code won’t benefit from auto-currying since we still need to «revert» the parameter order, but anyway
    );
}

This can’t impact all functions though, as it must not change their signature or internal logic, for example pipe() works quite differently than Ramda’s implementation, and we pass the data to it as the first argument, so I guess currying pipe() by default does not really makes sense in that context. And obviously, higher-order functions like the ones in the nspl\f namespace are not good candidates for this either, for obvious reasons. I guess trying to implement this only for nspl\a and nspl\op functions could be a good start.

Also, one way to keep strict backward compatibility could be to create «parallel» namespaces that would mirror the original ones, only their functions would return the curried versions of the original ones. For example we could create a nspl\c\op.php that would contain the nspl\c\op namespace, containing all the curried versions of the functions in nspl\op.

The implementation could stay very simple and reuse the lib’s existing curried() function, so for example:

namespace nspl\c\op;

use const nspl\op\sum as _sum;
use function nspl\f\curried;

function sum(...$args)
{
    $curriedSum = curried(_sum);

    foreach ($args as $arg) {
        $curriedSum = $curriedSum($arg);
    }

    return $curriedSum;
}

(This implementation is obviously stupid, but it serves illustration purposes only.)

What do you think? This is a very raw proposal and of course there is a lot of potential side-effects, so let’s discuss about it! 🙂

@Gert-dev
Copy link

Gert-dev commented Aug 1, 2019

Wow, this is a strange coincidence: I was just the other day thinking about how great it would be if PHP had currying at its core. It would even be mostly backwards compatible, save for code relying on ArgumentCountErrors being thrown and such 😄.

Thinking on this, I noticed that currying is probably what I am missing. I have been working around sprinkling partial everywhere by using the with function, like so:

<?php

private function calculateAverageRating(): float
{
    if (!count($this->ratings)) {
        return 0.0;
    }
​
    with($this->ratings)
        ->map(methodCaller('getScore')),
        ->reduce(sum) / count($this->ratings),
    );
}

The problem with this is, is that it will not work with the lazy variants, as I have now moved the division out of the function composition and thus required that the result be immediately evaluated, which does work nicely in your currying and original example.

I think your rpartial(div, count($this->ratings)) could possibly be rewritten to flipped(div)(count($this->ratings)) to benefit from currying, though flipped would probably need to be rewritten, as it blindly tries to immediately call the flipped function, which will then still pass the first argument as actual first argument if you don't supply all arguments immediately.

I wanted to raise the concern that there already is a separate version of most functions to make them lazy. Adding a separate curried version of all functions would probably explode into a whopping four versions of all functions. Though I must say that I have been wondering if it would be feasible to just drop the non-lazy variants of the library in a new major release altogether. They could then also be made curried and it would reduce maintenance costs considerably.

Evaluating lazy functions is automatically done by looping over them anyway and, if you really need arrays as output, iterator_to_array already exists (which, takes a Traversable, which Generator implements). I'm also skeptical of any major performance difference between generators and arrays, but I don't think it will make much of a difference unless you have huge collections, which I doubt will happen often.

Most functions are also wrapped in the ds.php classes for convenience. Though, if we get currying, it's pretty much as short as using the with keyword, and makes staying inside the function bubble less awkward, I'm wondering if it may not just be a better idea to drop ds.php as well.

Now if only we had operator overloading so I can overload the dot operator to do function composition and the bitwise or operator to pipe functions 😄.

@igneus
Copy link

igneus commented Mar 18, 2020

Evaluating lazy functions is automatically done by looping over them anyway and, if you really need arrays as output, iterator_to_array already exists (which, takes a Traversable, which Generator implements).

There are other libraries of similar purpose using iterators everywhere by default and they are really painful to work with in real-life scenarios. iterator_to_array calls all around the code, because most standard library functions don't accept iterators.

@igneus
Copy link

igneus commented Mar 18, 2020

Though I must say that I have been wondering if it would be feasible to just drop the non-lazy variants of the library in a new major release altogether.

You don't have any larger application using this library, do you?

@igneus
Copy link

igneus commented Mar 18, 2020

As for currying by default: if you like Ramda, there are already enough PHP libraries going the Ramda way (see my list). I think NSPL has a clear profile, is quite good at what it is, and shouldn't try to become something else with the next major release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants