Open codesections opened 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.
Add multi
s 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.
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".
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
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?
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.)
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.
Consider also having a way to partially apply keyword arguments, perhaps by passing a pair.
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:
Individuals do whatever they want to do to mutate the PL;
Individuals may publish their stuff if they want to;
Core compiler devs (which will realistically mean Rakudo devs for the foreseeable future) collectively decide what they're interested in doing, including considering enabling the best ecosystem stuff to work better and/or bundling it into the Rakudo distribution;
We argue among ourselves about what we want in future versions of the "standard" Raku slang collection, including adding new stuff and deprecating and dropping old stuff. jnthn will have enormous sway there by dint of being the lead dev of Rakudo.
Raku evolves to make it easier to mutate the language and package mutations up for adding to the ecosystem. Stuff like, oh, I dunno, macros, or even what I'll call "slangs". Especially if there were some nice clean standard AST one could generate, walk, and modify. Imagine it being possible to create packages that drop into the compiler as part of mutating the language! Sigh. Well, we can but dream... ;)
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)
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 aCallable
on its LHS (like.assuming
, leaving the newCallable
's arity as non-zero) or over supply arguments, in which case the over-supply is held over for application to the result of theCallable
, presuming it too isCallable
, 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.
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.
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.
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.
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?
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.
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?
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.
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
).
(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.
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.
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! :) )
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.
Raku makes many cases of partial application painless β whenever you can write the code with simple method calls or operators, Whatever-currying works well:
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
: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.