Raku / problem-solving

πŸ¦‹ Problem Solving, a repo for handling problems that require review, deliberation and possibly debate
Artistic License 2.0
70 stars 16 forks source link

"assuming" is a big word #290

Open codesections opened 3 years ago

codesections commented 3 years ago

Raku makes many cases of partial application painless – whenever you can write the code with simple method calls or operators, Whatever-currying works well:

my &sentences = *.split: '.  ';
my &square = *Β²; # though note that `* ** 2` is pretty bad

However, when the signatures are more complex/you need to be able to pass multiple arguments, Whatever-currying no longer works. In that instance, the tool Raku offers for partial application is the .assuming method. However, "assuming" is such a long method name that the .assuming method is effectively useless – Raku's built Block syntax already allows partial application in a way that's just as concise and considerably clearer than .assuming:

my &minmax-prime0 = *.first(&is-prime); # No, doesn't work for passing :end 
my &minmax-prime1 = &first.assuming(&is-prime);
my &minmax-prime2 =->|c { first &is-prime, |c }

In some ways, this is a testament to how lightweight Raku's inline Block syntax is, but it still seems that it would be nice to have a more concise and expressive alternative to .assuming. This is especially true given that many of Raku's peer languages either have auto-currying (which effectively gives partial application for free) or are considering a *-like placeholder syntax that would allow partial application by "calling" a function with the placeholder. (Though I think Raku made the right call in not giving * that behavior).

Solving this problem now seems especially helpful, given that the new-dispatch work means that assuming may get much faster, so I'd be nice to have syntax that encourages its use.

codesections commented 3 years ago

The two most obvious ways to address this issue are to either add a shorter synonym for .assuming or to create a new operator for partial application. However, both of these solutions has the problem of requiring us to find a name – and their don't seem to be any particularly strong/obvious contenders. I think we can do better.

Initial proposal

Add multis to infix:<∘> to allow partial application along the following lines:

multi infix:<∘>(&left, Capture \c) { &left.assuming: |c }
multi infix:<∘>(&left, +@args)     { &left.assuming: |@args }

This would allow the example above to be written as

my &minmax-prime = &first ∘ \(&is-prime);

and when the arguments for the partly-applied function don't include a callable, you could use even lighter syntax from the second multi candidate:

my &add1 = &sum ∘ 1;
# compare: * + 1

Even compared to the admittedly shorter and Whatever-currying, the proposed syntax strikes me as both more readable and as better capturing the code's intent. And, of course, the goal is to support use cases where *-currying isn't possible.

Additionally, this proposal would compose well with existing uses of the composition operator. (The example below was inspired by a recent IRC conversation and was originally a translation of a Haskell SCIP solution, which I've added in a comment on the last line.)

# Instead of this
my &odd-squares-product0 = &reduce.assuming(&[Γ—]) ∘ &map.assuming(*Β²) ∘ &grep.assuming(* !%% 2);
# we could write this
my &odd-squares-product1 = &reduce ∘ \(&[Γ—]) ∘ &map ∘ \{$_Β²} ∘ &grep ∘ \{$_ !%% 2};
# or, with different whitespace:
my &odd-squares-product1 = &reduce∘\(&[Γ—]) ∘ &map∘\(*Β²) ∘ &grep∘\(* !%% 2);
#   odd_squares_product2 = (foldl (*) 1) . (map (\x -> x^2)) . (filter odd)

I know TIMTOWTDI, but to my eye the bottom two expressions are much more readable, and compare favorably with the Haskell solution.

Potential drawbacks

This syntax might slightly bother functional purists, who may believe that ∘ should signify only functional composition. However, the proposed &sum ∘ 1 syntax is directly translatable to &sum ∘ ->|c { \(1, |c) } in existing syntax, and this slight amount of sugar seems like something even a purist could be happy with.

Similarly, in theory overloading operators can add to user confusion. However, the ∘ isn't an operator that people tend to reach for unless they've had some exposure to functional programming. And if they're familiar with ∘, it's hard for me to imagine that the would have trouble reading expressions like "sum composed with 1".

tony-o commented 3 years ago

I'm not sure I'm a fan of partial composition in the form of &sum o 1.. maybe something more explicit along the lines of &sum 1, |* or if you wanted to curry & compose partially &sum 1 o &Slip o &flat # sum(1, *.flat.Slip) (I realize this example doesn't really need currying, just a way to demonstrate). The curry symbol appears to be too conflated with the push. Another option might be &sum o &push: 1

codesections commented 3 years ago

I'm not sure I'm a fan of partial composition in the form of &sum o 1.

Could you say a bit more about what you dislike about that construction? If your concern is about explicitness, how would you feel about &sum ∘ \(1) (which I'd suggested as a way to pass &-sigiled arguments, but could be used for everything). I don't like it quite as much; it adds a bit of noise, and I find &sum ∘ 1 explicit enough. But &sum ∘ \(1) is (imo) very explicit – it reads as "the function 'sum' composed with the signature 'one'", which is exactly what it is.

The curry symbol appears to be too conflated with the push.

I don't understand what you mean here. What curry symbol? and how is is similar to push?

moon-chilled commented 3 years ago

This syntax might slightly bother functional purists, who may believe that ∘ should signify only functional composition

Non-callables are actually constant functions that always return themselves :)

I will also note that ∘ as partial application has precedent in APL. (And the constant self-returning function can be found in APL too, if you dig deeply enough.)

moon-chilled commented 3 years ago

Consider also a form x∘&f, where (x∘&f)(|xs) is equivalent to f(|xs, x); that is, it binds the last positional argument rather than the first.

moon-chilled commented 3 years ago

Consider also having a way to partially apply keyword arguments, perhaps by passing a pair.

raiph commented 3 years ago

But &sum ∘ \(1) is (imo) very explicit – it reads as "the function 'sum' composed with the signature 'one'", which is exactly what it is.

Perhaps that was just a thinko, but to be clear it would be the function composed with a Capture, which is more like the opposite of a signature.

But I get the idea, and something like it does seem appealing.


I can imagine it being desirable to have two variants, with one supporting partial binding, consistent with .assuming, and the other required to fully bind, so can't be partial, like a .assuming that is required to provide a value for all parameters that require them, rendering a new function that doesn't require any more arguments.

If one leaves \(...) for full, then perhaps something like this for partial:

role Partial {}
sub circumfix:< \[ ] > (|capture) { \(|((|capture)[0])) does Partial }
say $_, .WHAT given \[ 42, :a ]; # 
multi infix:<o> (\lhs, Partial \rhs) { lhs.assuming: |rhs }

And/or perhaps &sum[1, 2, 3] is short for &sum .assuming: 1, 2, 3?


tony-o's reaction speaks to the process of dealing with any proposed change to Raku.

Your idea is not a no-brainer. I love it, or something like it. But I'm hesitant to think of it being added to standard Raku prior to it getting a year or three trial. Others will have their reactions. We'd want the community to essentially universally want any new feature, and to do so based on sufficient proof that it's a good idea, which raises the issue of what that process should / will look like.

Imo it should be built on the Raku promise, which is a principled new generation of CPAN, an open ecosystem as the PL's driving force. And then:

FCO commented 3 years ago

I was wondering... Would it make any sense to make something like this:

my &func = -> $a -> $b, $c -> $d { "$a $b $c $d" }

be equivalent to

my &func = -> $a {
   -> $b, $c {
      -> $d {
         "$a $b $c $d"
      }
   }
}

and make the [] post circunfix operator (or any other postcircunfix operator) on Callable work like getting as many arguments as the function expects from the list and passing the rest for the returning function? like:

func[1, 2, 3, 4]

would be equivalent to

func(1).(2, 3).(4)

Would that make any sense?

(it could also keep trying to match any maned parameter passed)

raiph commented 3 years ago

make the [] post circunfix operator (or any other postcircunfix operator) on Callable work like getting as many arguments as the function expects from the list and passing the rest for the returning function? like:

fun[1, 2, 3, 4]

would be equivalent to

fun(1).(2, 3).(4)

Ah. Interesting. So iiuc, that's a generalization of my suggestion such that the Capture implied by the [...] in &foo[...] can either under supply arguments to a Callable on its LHS (like .assuming, leaving the new Callable's arity as non-zero) or over supply arguments, in which case the over-supply is held over for application to the result of the Callable, presuming it too is Callable, in which case the args-to-callable process repeats until the args are exhausted, yielding a new function reference (that has not yet been called). Right?

(If by fun[...] you mean for that to also do the call, well, that presumably won't work because that's already meaningful syntax in practice, where fun returns a result which is reasonably indexable by a [...] circumscript.)

I'll be curious to read what @codesections has to say about these ideas.

raiph commented 3 years ago

From a practical perspective we're somewhat hemmed in by some past decisions, including making &foo(...) mean the same as foo(...), and also foo[...] and foo{...} having meanings.

I think the latter are very appropriate.

The &foo(...) meaning is more debateable. It could perhaps have been .assuming from the get go, and now be deprecated with a very long timeframe (eg a decade, or perhaps just 5 years) in order to get there in retrospect.

But I guess it made a lot more sense when Raku was primarily conceived as needing to focus essentially all its energy on pleasing Perl devs that it would be a call, just as it would in Perl.

It still makes sense even without that -- a call is ultimately a derefence, and a postfix (...) is one of Raku's call syntaxes -- but I also think it could make more sense to read the & sigil as very loudly emphasizing the reference to function aspect, so much so as to make &foo(...) still imply construction of another reference, not a call.

TimToady commented 3 years ago

I would add my vote to go slow on such a change.

From a language design perspective, there are a couple of arguments against greater concision here:

1) We optimize for readers of the code before we optimize for the

writers of code. Mere mortals already find partial function application deeply mystical, so "assuming" serves as a big fat clue that something tricky is going on.

2) We also aren't optimizing for code golf, which means you probably

shouldn't add definitions that are going to be used only once unless you can give them a fabulously informative name. And if you're going to be calling a function many times, it's the call site that needs to be concise, while the syntactic overhead of the definition is amortized over all the call sites, so it can afford to be a little heavier.

That said, yes, we also make it relatively easy (compared to most languages) to mutate the grammar on the fly, so modules gonna modulate. The whole point of allowing language mutability is to design in the ability of natural languages to evolve. If geezers like me end up cussing out the latest sloppy slang, that's just how it should be.

Larry

On Fri, Sep 17, 2021 at 5:52 AM raiph @.***> wrote:

From a practical perspective we're somewhat hemmed in by some past decisions, including making &foo(...) mean the same as foo(...), and also foo[...] and foo{...} having meanings.

I think the latter are very appropriate.

The &foo(...) meaning is more debateable. It could perhaps have been .assuming from the get go, and now be deprecated with a very long timeframe (eg a decade, or perhaps just 5 years) in order to get there in retrospect.

But I guess it made a lot more sense when Raku was primarily conceived as needing to focus essentially all its energy on pleasing Perl devs that it would be a call, just as it would in Perl.

It still makes sense even without that -- a call is ultimately a derefence, and a postfix (...) is one of Raku's call syntaxes -- but I also think it could make more sense to read the & sigil as very loudly emphasizing the reference to function aspect, so much so as to make &foo(...) still imply construction of another reference, not a call.

β€” You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/Raku/problem-solving/issues/290#issuecomment-921772381, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABHSYQLKNE4GQO4C2CJOZ3UCM2YDANCNFSM5AHGKVYA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

codesections commented 3 years ago

You all make some excellent points and provide more than enough justification for proceeding slowly.

However, I would like to push back on one point:

which means you probably shouldn't add definitions that are going to be used only once unless you can give them a fabulously informative name. And if you're going to be calling a function many times, it's the call site that needs to be concise

That seems to apply 100% for many function definitions, but not necessarily for those with very limited lexical scope. Consider the following currently valid code that iterates over an array of hashes:

 sub some-larger-task(@people) {
    # A few lines of code
    my &can-vote = *.<age> β‰₯ 18;
    my @voters = @people.grep(&can-vote);
    # just a few more lines
}

Now, if &can-vote lived past the enclosing scope, its name would pretty poorly chosen – all it does is check the age key, which is hardly the only requirement for voting eligibility. And it assumes that there is an age key, and that the voting age is always 18, both pretty questionable assumptions.

But none of that matters, because &can-vote doesn't exist outside of that lexical scope; it was never intended to be a reusable function, but just to give a name to the operation that grep is performing. The programmer who uses whatever currying to write that &can-vote function isn't golfing their code – in fact, they've made it longer than it would be if they just passed a {.<age> β‰₯ 18} closure directly to grep. But (imo, anyway) they've made their intent clearer and their code more readable as a result.

There are times when defining a function is like teaching the compiler (and human readers of the code) a new word that they should add to their vocabulary going forward. But there are other times when it's more like parenthetically noting a three letter acronym ("TLA") that they should remember for the next paragraph or so, but can freely forget again afterwords.

Raku currently makes it very easy to define that sort of I'll-use-it-for-now short lived function with Whatever currying – but only when the * can be in term position. I think that, in the long run, it'd be nice to have similar currying available for situations where the Whatever-shaped-hole is in an argument position.

But, again, lots of good points up thread about reasons to be careful and experiment thoroughly in this area.

raiph commented 3 years ago

Raku currently makes it very easy to define that sort of I'll-use-it-for-now short lived function with Whatever currying – but only when the * can be in term position. I think that, in the long run, it'd be nice to have similar currying available for situations where the Whatever-shaped-hole is in an argument position.

Er, I'm confused. Arguments are in term position. Are you talking about the fact that the compiler does not treat a * argument as cause to Whatever-curry its enclosing expression despite it being in term position if the `is just a term in a list separated by the,` operator? Per a reddit comment of mine, only unary and binary operators ever do currying, and while they do so by default, 6 of the built-in binary ops in current Raku opt out* of doing so:

The operators , and = and := interpret a * operand on either side as simply a non-meaningful *. ... If a binary operator does not have a * parameter matching a * argument, which includes all built in binary operators except the above six, then the compiler does the "whatever currying" that one sees with, for example, method calls like *.sum. Afaik this is the full set of "wrinkly" built in operators, and it's pretty unlikely to ever change, and somewhat unlikely to ever be extended.

Does that make any difference @codesections?

codesections commented 3 years ago

Er, yes – you're absolutely right that I wasn't using the term "term position" correctly.Β  I probably should have said "as an operand (with most operators, including the method-call operator)".

The distinction I'm pointing to is the one between my &f = 2 Γ— * (legal) and my &g = &infix:<Γ—>(2, *) (illegal without .assuming). I know why the later syntax can't be allowed – it would clash with actually passing a Whatever, which is very useful behavior.Β  But it makes me wonder if there's some way to extend the charm of Whatever currying to non-operators, which prompted the thoughts in this issue.

raiph commented 3 years ago

Hi again @codesections,

Thanks for clarifying; I'm still confused, but convinced we've discussed that detail enough. :)

I'm curious about why you posted this in problem-solving, and whether you agree with:

We'd want the community to essentially universally want any new feature, and to do so based on sufficient proof that it's a good idea, which raises the issue of what that process should / will look like. Imo it should be built on the Raku promise, which is a principled new generation of CPAN, an open ecosystem as the PL's driving force.

More specifically, do you agree it would make sense for you to create a package that implements some of your ideas related to this (I personally liked your idea of overloading composition of functions with composition with arguments/capture); use whatever you implement for a while for fun; post it to your github account; add it to fez; blog about it; continue discussions in its GH issues queue; if it works out, use it for longer in anger; polish it (tests/examples/doc/etc); make it a 1.0 when you think it's ready for prime-time; sustain it; register it with toast (or whatever it is we have that tests each Rakudo commit against ecosystem packages to ensure their tests aren't broken); and so on, for a year or three, before determining whether you think there's a case to be made that it should be in standard Raku?

codesections commented 3 years ago

I'm curious about why you posted this in problem-solving

I posted this in problem solving to start a conversation about whether something that I view as a "problem" (or, at least, as something that would benefit from a solution) is something that others also view that way and, if so, to see what ideas others might have for addressing it. It could have been the case that many others reported that they aren't troubled by the length of .assuming at all, in which case I'd have entirely have dropped the idea of adding any shorter construct to the language. (Though I might have pursued something that's 100% in module space.) Or someone could have come up with an idea for solving the problem that we all can recognize as much better than anything I came up with. In short, I view opening an issue here as a good first step (unlike opening a PR here, which comes much later in the process.)

Do you have a different view on when opening Problem Solving issues makes sense?

More specifically, do you agree it would make sense for you to create a package that implements some of your ideas related to this…; use whatever you implement for a while for fun; post it to your github account; add it to fez; blog about it; continue discussions in its GH issues queue; if it works out, use it for longer in anger; polish it (tests/examples/doc/etc); make it a 1.0 when you think it's ready for prime-time; sustain it; register it with [blin; and so on, for a year or three, before determining whether you think there's a case to be made that it should be in standard Raku?

Based on the discussion so far, I think that's a good plan. (Well, I might quibble a bit with some of the details – in particular, I'd probably "use it for six months or so before determining whether there's a case to be made for adding it as experimental Raku feature" instead of going directly to standard Raku after a longer testing period. But that doesn't change the general idea.)

I don't tend to think that it would have been a good idea to go straight to creating an independent package without discussing it here first. That sequence could certainly work, of course, but (imo at least) it's better to solicit feedback at the outset – and thereby benefit from any ideas or feedback that others might have at the design stage.

MasterDuke17 commented 3 years ago

It could have been the case that many others reported that they aren't troubled by the length of .assuming

FWIW, that's pretty much the camp I fall into (however, that's probably because I don't think I've ever used .assuming).

codesections commented 3 years ago

(however, that's probably because I don't think I've ever used .assuming).

I also don't use .assuming all that often – but that's because I tend to just use a closure. sub name(|c) { fn $arg, |c } is basically as short and (imo) more readable than my &name = &fn.assuming: $arg;, so I tend to just use the closure. But I'd still like to see something that's clearer/shorter/more readable than either.

CIAvash commented 3 years ago

I agree that assuming is long, but it's not a problem if you use it a few time here and there, but it can be a problem if you look at the original example @codesections showed from IRC log:

my &mul_odd_squares = &reduce.assuming(&[Γ—]) ∘ &map.assuming(*Β²) ∘ &grep.assuming(* !%% 2)

2 lines below it, I also wrote it with the feed operator:

 [1..5] ==> grep(* !%% 2) ==> map(*Β²) ==> reduce(&[Γ—]) ==> say()

Looking at it from another perspective, would it be possible for ∘ to kind of act like the feed operator? So instead of all those assuming, we'd write something like:

my &mul_odd_squares = reduce(&[Γ—]) ∘ map(*Β²) ∘ grep(* !%% 2)

If we continue the comparison with the feed operator, we can say the feed operator doesn't just feed things to functions, it also pushes things to pushable objects:

my @a = 1, 2, 3;
@a.push: 4; # Array @a = [1, 2, 3, 4]
5 ==> @a;   # Array @a = [1, 2, 3, 4, 5]

Does it mean that the feed operator is just a shortcut for pushing, or a push operator? No. Similarly I think the function composition operator can take arguments and give them to a function and compose a new function.

Just a thought.

raiph commented 3 years ago

Similarly I think the function composition operator can take arguments and give them to a function and compose a new function.

At first encounter I absolutely love this. Imo it's a huge improvement over prior suggestions, in fact I find it hard to conceive how one might more perfectly distil Daniel's basic point, and their idea of putting a classic Perlish/Larryian twist on things that would be seen as sacrosanct in other PL cultures, namely evolving use of infix o. What you've shown has sent shivers down my spine. Maybe someone will point out what's wrong with it, but for now it seems sublime to me.

.oO ( Of course, it would require infix o to be a macro. Which would mean we need macros. Oh dear. :( Oh! :) )

TimToady commented 3 years ago

Despite my earlier go-slow advice, I will point out on the other side that the big reason we distinguished Any from Mu is so that conceptual types such as Junction could live outside of Any, to be interpreted outside the standard object types by code that knows how to apply the concept in question. So maybe that's the space you should be aiming your solutions into.

Larry

On Sat, Sep 25, 2021 at 1:58 PM raiph @.***> wrote:

Similarly I think the function composition operator can take arguments and give them to a function and compose a new function.

At first encounter I absolutely love this. Imo it's a huge improvement over prior suggestions, in fact I find it hard to conceive how one might more perfectly distil Daniel's basic point, and their idea of putting a classic Perlish/Larryian twist on things that would be seen as sacrosanct in other PL cultures, namely evolving use of infix o. What you've shown has sent shivers down my spine. Maybe someone will point out what's wrong with it, but for now it seems sublime to me.

.oO ( Of course, it would require infix o to be a macro. Which would mean we need macros. Oh dear. :( Oh! :) )

β€” You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Raku/problem-solving/issues/290#issuecomment-927181954, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABHSYTR6Z2CXWIIDRRASYDUDYZVVANCNFSM5AHGKVYA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.