hsutter / cppfront

A personal experimental C++ Syntax 2 -> Syntax 1 compiler
Other
5.48k stars 243 forks source link

[SUGGESTION] Implement the pipeline operator from P2011 & P2672 #741

Open bluetarpmedia opened 1 year ago

bluetarpmedia commented 1 year ago

Suggestion Cpp2 could support the pipeline operator |> as proposed in P2011 and further explored in P2672.

Specifically, the pipeline operator with the "placeholder model" with mandatory placeholder (e.g. $), as described in P2672 section 6 Disposition. Both papers explain the problem and motivation for the new operator, as well as discussing options for the placeholder token.

Circle uses $ as its token.

The proposed operator enables a simpler left-to-right style as opposed to an inside-out style.

Conor Hoekstra (code_report) has various talks about ranges and pipelines and explains how the pipeline operator can make the code simpler and more readable. The following is one of his examples:

Without the operator:

auto filter_out_html_tags(std::string_view sv) {
    auto angle_bracket_mask = 
        sv | rv::transform([](auto e) { return e == '<' or e == '>'; });
    return rv::zip(rv::zip_with(std::logical_or{}, 
            angle_bracket_mask, 
            angle_bracket_mask | rv::partial_sum(std::not_equal_to{})), sv)
        | rv::filter([](auto t) { return not std::get<0>(t); })
        | rv::transform([](auto t) { return std::get<1>(t); })
        | ranges::to<std::string>;
}

With the operator:

auto filter_out_html_tags(std::string_view sv) {
    return sv 
        |> transform($, [](auto e) { return e == '<' or e == '>'; })
        |> zip_transform(std::logical_or{}, $, scan_left($, true, std::not_equal_to{}))
        |> zip($, sv)
        |> filter($, [](auto t) { return not std::get<0>(t); })
        |> values($)
        |> ranges::to<std::string>($);
}

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code? No

Will your feature suggestion automate or eliminate X% of current C++ guidance literature? No

Describe alternatives you've considered. Alternatives are discussed at length in the two papers referenced above.

JohelEGP commented 1 year ago

The talk: "New Algorithms in C++23 - Conor Hoekstra - C++ on Sea 2023". The recommended version: "Guide to the New Algorithms in C++23 - Conor Hoekstra - CppNorth 2023".

hsutter commented 1 year ago

The proposed operator enables a simpler left-to-right style as opposed to an inside-out style.

As soon as I see this, I think "hmm, like UFCS does?", and then I saw the example which looks suspiciously like UFCS for most of the cases.

Clarifying questions, just so I understand the question (I haven't had time to watch the talk).

Is the $ is the placeholder for where to put the left-hand side of the operator, so that this code (ignoring for now the zip_transform one where it's not in the first location):

    return sv 
        |> transform($, [](auto e) { return e == '<' or e == '>'; })
        |> zip($, sv)
        |> filter($, [](auto t) { return not std::get<0>(t); })
        |> values($)
        |> ranges::to<std::string>($);

would be the same as this using UFCS, which I think would work in Cpp2 now:

    return sv 
        .transform(:(e) e == '<' || e == '>';)
        .zip(sv)
        .filter(:(t) !std::get<0>(t);)
        .values()
        .ranges::to<std::string>();

or something like that, modulo any late-night typos I wrote?

JohelEGP commented 1 year ago

That's right.

Including zip_transform, I have confirmed that this translates correctly (https://cpp2.godbolt.org/z/7Wdbf5o1Y, https://compiler-explorer.com/z/hcx9ex4j4 [formatted]):

#include <algorithm>
#include <ranges>
using namespace std::views;
// auto filter_out_html_tags(std::string_view sv) {
//     return sv 
//         |> transform($, [](auto e) { return e == '<' or e == '>'; })
//         |> zip_transform(std::logical_or{}, $, scan_left($, true, std::not_equal_to{}))
//         |> zip($, sv)
//         |> filter($, [](auto t) { return not std::get<0>(t); })
//         |> values($)
//         |> ranges::to<std::string>($);
// }
filter_out_html_tags_cpp2: (sv: std::string_view) -> _ = {
  (a := sv
        .transform(:(e) e == '<' || e == '>';))
  return zip_transform(std::logical_or(), a, scan_left(a, true, std::not_equal_to()))
        .zip(sv)
        .filter(:(t) !std::get<0>(t);)
        .values()
        .to<std::string>();
}
main: () = { }

Can you remind me where scan_left comes from?

bluetarpmedia commented 1 year ago

Can you remind me where scan_left comes from?

scan_left is from code_report's example.

auto scan_left(auto rng, auto init, auto op) {
    return transform(rng, [first = true, acc = init, op](auto e) mutable {
            if (first) first = false; 
            else acc = op(acc, e);
            return acc;
        });
}
hsutter commented 1 year ago

Wow, that was fast. I was just trying to write the code for zip_transform a different way, but I see a statement parameter worked.

What about expressing this

    |> zip_transform(std::logical_or{}, $, scan_left($, true, std::not_equal_to{}))

as this

    . :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

?

Then it's all . ?

(I haven't tried to compile the code though.)

bluetarpmedia commented 1 year ago

There's some overlap between UFCS and this proposed pipeline operator.

Conor's presentation shows off various range pipeline examples, but the P2011 and P2672 papers discuss the motivation and problem space.

P2011 also discusses how it's different to UFCS. I think the key difference for Cpp2 is allowing the placeholder to appear in different argument positions in order to compose the range algorithms and views.

JohelEGP commented 1 year ago

Do you have a test? This compiles: https://cpp2.godbolt.org/z/n8673xofn.

I had to add to, because Libstdc++ doesn't have ranges::to. Also, Libc++ doesn't implement zip_transform. (https://en.cppreference.com/w/cpp/compiler_support).

JohelEGP commented 1 year ago

By the way, I just took the parameter of to by in. The error message was hideous. I compiled locally with #506 (and some other things) and quickly found out

x.cpp2:4:104: error: no match for call to ‘(to<std::__cxx11::basic_string<char>, std::ranges::elements_view<…

( inserted by me). These are the sizes of the error outputs:

$ ls out-* -lh
-rw-r--r-- 1 johel johel 60K Oct  9 21:28 out-main
-rw-r--r-- 1 johel johel 23K Oct  9 21:27 out-waarudo
JohelEGP commented 1 year ago

Do you have a test?

I added one. It seems to output characters rather than strings. https://cpp2.godbolt.org/z/s5crPezaj.

JohelEGP commented 1 year ago

. :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

That's not valid grammar (https://cpp2.godbolt.org/z/9avjYh6sb):

main.cpp2...
main.cpp2(25,10): error: '.' must be followed by a valid member name (at '(')
hsutter commented 1 year ago

Yes -- in a racing update I was updating the comment to say the following, but I'll make it a separate reply instead:

Right, that code is currently rejected because . must be followed by a name. So perhaps have a general helper like call:(f) :(x) f(x); to enable writing

    .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()));)

modulo typos and bugs? Or maybe name it curry? Anyway, signing off for tonight, but a very interesting question! Thanks.

bluetarpmedia commented 1 year ago

You're right that UFCS is very close already, and arguably the dot syntax is just as nice as the new operator, but with UFCS I'd still prefer a token to make the syntax simpler. (Perhaps _ is better than Circle's choice of $ since there's already precedent in Cpp2.)

.zip_transform(std::logical_or{}, _, scan_left(_, true, std::not_equal_to{}))

JohelEGP commented 1 year ago

Excellent. It works again with call:(forward o, forward f) f(o); (https://cpp2.godbolt.org/z/evj8PnzE7). Circle:

auto filter_out_html_tags(std::string_view sv) {
    return sv 
        |> transform($, [](auto e) { return e == '<' or e == '>'; })
        |> zip_transform(std::logical_or{}, $, scan_left($, true, std::not_equal_to{}))
        |> zip($, sv)
        |> filter($, [](auto t) { return not std::get<0>(t); })
        |> values($)
        |> ranges::to<std::string>($);
}

Cpp2 (colored): 1697982213

Text ```Cpp2 filter_out_html_tags_cpp2: (sv: std::string_view) // sv.transform(:(e) e == '<' || e == '>') .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()))) .zip(sv) .filter(:(t) !t.get<0>()) .values() .to(); ```
JohelEGP commented 1 year ago

IIRC, that proposal has seen push back due to having to specify the semantics of pipe arguments as non-first argument and of multiple pipe arguments in the same function call.

AbhinavK00 commented 1 year ago

Since there's talk about P2672, is there any interest for placeholder lambdas to replace the recently added :(x) x syntax?

JohelEGP commented 1 year ago

You're right that UFCS is very close already, and arguably the dot syntax is just as nice as the new operator, but with UFCS I'd still prefer a token to make the syntax simpler. (Perhaps _ is better than Circle's choice of $ since there's already precedent in Cpp2.)

.zip_transform(std::logical_or{}, _, scan_left(_, true, std::not_equal_to{}))

Standalone $, specially as an argument, has no meaning in Cpp2. It could mean "capture the object argument here" in a function call expression. And so the default becomes today's "capture the object argument as the first argument". "Object argument" means the expression before the ..

So when you write x.f(args), you get the default for x.f($, args). UFCS is then defined to apply when the first argument is $.

We still have the problem of having to specify the behavior when the object argument appears more than once or it appears as more than a simple $, e.g., $.first.

Anyways, I think a simple $ argument plays well with https://github.com/hsutter/cppfront/wiki/Design-note%3A-Capture and https://github.com/hsutter/cppfront/wiki/Design-note%3A-Defaults-are-one-way-to-say-the-same-thing.

JohelEGP commented 1 year ago

Do you have a test?

I added one. It seems to output characters rather than strings. https://cpp2.godbolt.org/z/s5crPezaj.

Looks like it works on characters, indeed. Using the fixed implementation from https://github.com/hsutter/cppfront/issues/741#issuecomment-1754194152, https://cpp2.godbolt.org/z/qjvonb8s3, it prints the same as the CE link from the talk at codereport/Content: https://godbolt.org/z/on5xMG5ax.

JohelEGP commented 1 year ago

@codereport FYI.

hsutter commented 1 year ago

I wish I could use the terse function syntax, but main.cpp2: error: unexpected end of source file.

Thanks! It's rare these days that I find a bug in the very first "load" step that tags which code is Cpp1 vs Cpp2, but this was one. I think it's fixed in this commit: 789cd382ed4c2fb1a9e306e73b6876228d22207d

is there any interest for placeholder lambdas to replace the recently added :(x) x syntax?

Do you mean like Boost.Lambda's _1 + f() ? If so...

My concern with that is, would it be:

I could be persuaded to like _1-style placeholders, though, if these two things could be addressed:

Does that make sense?

hsutter commented 1 year ago

I don't seem to have a C++ compiler installed on this machine that supports all of the new range/view things used in this example, because I'm mainly testing with a-few-years-old compilers to ensure compatibility.

But if I understand correctly, the original example of this:

auto filter_out_html_tags(std::string_view sv) {
    auto angle_bracket_mask = 
        sv | rv::transform([](auto e) { return e == '<' or e == '>'; });
    return rv::zip(rv::zip_with(std::logical_or{}, 
            angle_bracket_mask, 
            angle_bracket_mask | rv::partial_sum(std::not_equal_to{})), sv)
        | rv::filter([](auto t) { return not std::get<0>(t); })
        | rv::transform([](auto t) { return std::get<1>(t); })
        | ranges::to<std::string>;
}

which could be written more simply using the proposed |> operator like this:

auto filter_out_html_tags(std::string_view sv) {
    return sv 
        |> transform($, [](auto e) { return e == '<' or e == '>'; })
        |> zip_transform(std::logical_or{}, $, scan_left($, true, std::not_equal_to{}))
        |> zip($, sv)
        |> filter($, [](auto t) { return not std::get<0>(t); })
        |> values($)
        |> ranges::to<std::string>($);
}

works in Cpp2/cppfront today using just UFCS like this (with a helper call:(forward o, forward f) f(o);):

filter_out_html_tags_cpp2: (sv: std::string_view) -> _ = {
    return sv
        .transform(:(e) e == '<' || e == '>';)
        .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()));)
        .zip(sv)
        .filter(:(t) !t.get<0>();)
        .values()
        .to<std::string>();
}

... Is that correct?

JohelEGP commented 1 year ago

That's right.

I wish I could use the terse function syntax, but main.cpp2: error: unexpected end of source file.

Thanks! It's rare these days that I find a bug in the very first "load" step that tags which code is Cpp1 vs Cpp2, but this was one. I think it's fixed in this commit: 789cd38

Yes, this works now.

filter_out_html_tags_cpp2: (sv: std::string_view) //
  sv.transform(:(e) e == '<' || e == '>';)
    .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()));)
    .zip(sv)
    .filter(:(t) !t.get<0>();)
    .values()
    .to<std::string>();
hsutter commented 1 year ago

[Edited to add that this also helps reduce need for library techniques like overloading |]

Groovy, thanks.

I've learned two major things from this thread:

JohelEGP commented 1 year ago

Opened #746 for this. Something I've mentioned before is that UFCS on a qualified name doesn't work with GCC: https://compiler-explorer.com/z/qb19TEGv1. And Cpp2 doesn't have using declarations: #559. So we'd have to put using ranges::to at global scope, outside any namespace, possibly far from its use. I can do that now that I realized that the ranges::to in the code was range-v3's and not std's. But maybe it's just a GCC bug.

AbhinavK00 commented 1 year ago

I wish I could use the terse function syntax, but main.cpp2: error: unexpected end of source file.

Thanks! It's rare these days that I find a bug in the very first "load" step that tags which code is Cpp1 vs Cpp2, but this was one. I think it's fixed in this commit: 789cd38

is there any interest for placeholder lambdas to replace the recently added :(x) x syntax?

Do you mean like Boost.Lambda's _1 + f() ? If so...

My concern with that is, would it be:

  • Would it be a special feature that works only in anonymous function bodies?
  • Would it be allowing a second way to say the same thing (not just defaulting) -- a competing syntax to teach, and one that meets overlapping needs so we would have to teach which to use when? Whereas the current syntax for lambdas is still a single syntax with optional parts you can omit when you're not using them.

I could be persuaded to like _1-style placeholders, though, if these two things could be addressed:

  • If they could serve a general purpose in the language beyond anonymous function placeholder parameters, just like $ for capture works for "capture value" semantics everywhere (not just in anonymous function captures, but also postconditions and string interpolation).
  • If that general use were allowed in ordinary named functions in a way that still naturally lets us omit unused parts of the general function syntax to get down to anonymous functions, so we still have a single function syntax.

Does that make sense?

Well, my thinking was that the current new syntax was already kind of divergent. Yes, it is obtained by omitting parts but that ommitance is conflicting (omitting -> Type means void return type while omitting -> _ = means deduced return type.) Furthermore, this syntax would make it's way to full (named) function declarations where it's desirable for full parts to be present. That conflicts with your 2nd point but that's how I see it. Most languages which have a short lambda syntax do not allow omitting from full functions. As for the more general presence of placeholders, if the pipeline operator is present, that'd be one place for them in the language but I got nothing on this front. Maybe some places in the language can be tweaked for this (like its done with string interpolation, it easily could've been done with format library but $ was used because it was present more generally in cpp2). It'll not be competing because it'd only work for anonymous functions, maybe there's no need to start the with :.

JohelEGP commented 1 year ago

Well, my thinking was that the current new syntax was already kind of divergent. Yes, it is obtained by omitting parts but that ommitance is conflicting (omitting -> Type means void return type while omitting -> _ = means deduced return type.)

That's because when you're using the terse syntax, the default you're writing for is -> _ =. See https://github.com/hsutter/cppfront/wiki/Design-note%3A-Defaults-are-one-way-to-say-the-same-thing.

To me, allowing a generic function f:(i:_) -> _ = { return i+1; } to be spelled f:(i) i+1; is like that... there's only one way to spell it, but you get to omit parts where you're happy with the defaults.

JohelEGP commented 1 year ago

You may be interested in reading

WG21 Number Title Author
P3021 Unified function call syntax (UFCS) Herb Sutter

which mentions

This paper was motivated by [cppfront #741]

JohelEGP commented 1 year ago

After reading the paper above, I once again thought of this:

. :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

That's not valid grammar (https://cpp2.godbolt.org/z/9avjYh6sb):

main.cpp2...
main.cpp2(25,10): error: '.' must be followed by a valid member name (at '(')

We have UFCS with semantics obj.func(), that if not valid, is rewritten as func(obj). So now it makes sense for func to also be a function expression. Without UFCS, obj.:(x) x;() makes no sense (or obj.[](auto x) { return x; }() in Cpp1). But with UFCS, that has a meaning, except that it's not valid grammar.

gregmarr commented 1 year ago

I find it very interesting that it takes only a single sentence in standardese to enable UFCS for x.f(...) to call f(x, ...).

If E2 is not found as a postfix function call expression using the dot operator, and E1.E2 is followed by a function argument list (args), treat the postfix expression E1.E2(args) as a postfix function call expression E2(E1,args).

hsutter commented 1 year ago

. :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

That's not valid grammar (https://cpp2.godbolt.org/z/9avjYh6sb):

Right, I like the call helper well enough that I'm waiting to see if there's really a need to write a new function expression in the middle of a postfix-expression... it's doable but is it needed?

I find it very interesting that it takes only a single sentence in standardese to enable UFCS for x.f(...) to call f(x, ...).

Once you have both things specified, it's actually fairly easy in the first thing's specification to turn "else it's ill-formed" into "else try [second existing thing]"... great example of reuse. Same thing in cppfront, when all I have to do for a new feature is enable also looking at another existing thing (e.g., grammar production) it tends to be a few-line tactical change rather than a big surgery.

That said, note that is only my own draft standardese wording, I still need a card-carrying Core language working group expert to check it 😏 . That said, it is based on language that ware Core-reviewed when a variation of this proposal last made it to plenary in 2016, just it was going the other (IMO wrong) way, having f(x) fall back to x.f().

JohelEGP commented 11 months ago

With regards to https://www.reddit.com/r/cpp/comments/17h7trm/p3027r0_ufcs_is_a_breaking_change_of_the/, how about changing the UFCS syntax to from using . to using .:? E.g., obj.:func(args). The : in .: comes from the ::s in the (implicit) name of a chosen non-member func (e.g., could be ::func, ns::func when within ns, or the one in obj.:base::func(args)).

I think the ADL woes, regardless of UFCS, are an orthogonal issue, best solved separately.

SebastianTroy commented 11 months ago

All code written in cpp2 is new code, so ufcs can't be a breaking change for cpp2 code?

On 11 November 2023 15:31:38 Johel Ernesto Guerrero Peña @.***> wrote:

With regards to https://www.reddit.com/r/cpp/comments/17h7trm/p3027r0_ufcs_is_a_breaking_change_of_the/, how about changing the UFCS syntax to from using . to using .:? E.g., obj.:func(args). The : in .: comes from the ::s in the (implicit) name of a chosen non-member func (e.g., could be ::func, ns::func when within ns, or the one in obj.:base::func(args)https://github.com/hsutter/cppfront/issues/746#issuecomment-1758893025).

I think the ADL woes, regardless of UFCS, are an orthogonal issue, best solved separately.

— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/741#issuecomment-1806845249, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQNGNI3EII2QTDSK2BDYD6K5PAVCNFSM6AAAAAA5ZQQZ32VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMBWHA2DKMRUHE. You are receiving this because you are subscribed to this thread.Message ID: @.***>

JohelEGP commented 11 months ago

All code written in cpp2 is new code, so ufcs can't be a breaking change for cpp2 code?

Yeah, and the counter paper recognizes this. It also raises engineering concerns that apply regardless of syntax.

Moving code from Cpp1 to Cpp2 can introduce a breaking change. A member call that SFINAEd out in Cpp1 will try to call a non-member in Cpp2. You can build cases where that changes meaning (code becomes ill-formed, or silently changes meaning).

JohelEGP commented 11 months ago

. :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

That's not valid grammar (https://cpp2.godbolt.org/z/9avjYh6sb):

Right, I like the call helper well enough that I'm waiting to see if there's really a need to write a new function expression in the middle of a postfix-expression... it's doable but is it needed?

You could say it's needed for #748, so we could write w: (f) :(x) x.(f$)(); and have x.(f$)() call (f$)(x).

MaxSagebaum commented 11 months ago

In my opinion, having a different symbol for UFCS would reduce surprises and would make it an opt-in. So I strongly support this notion.

msadeqhe commented 11 months ago

I think |> is a valuable proposal to consider, it's like UFCS with explicit this, but with a new syntax which it doesn't have any advantage to existing code:

a |> function($, b, c) // Proposal
a .  function(   b, c) // UFCS

Cpp2 can have the good part of the proposal: The placeholder $. The placeholder allows us to use UFCS with explicit this.

The placeholder $ have to be changed to another syntax in Cpp2, because $ is the notation to capture variables. Let's assume the placeholder notation is it keyword in Cpp2.

Optional Placeholder

Let's consider the placeholder is optional.

If we don't pass it keyword as an argument in UFCS, this is implicitly the first argument.

Also it keyword can be generalized to refer to the left operand of a binary operation. For example:

var1: = (x + y) * (x + y);
// It's the same as above. `it` is `(x + y)`.
var2: = (x + y) * it;

// The first  `it` is `(x + y)`.
// The second `it` is `(x + y) * it`.
var3: = (x + y) * it * it;

var4: = ab.fnc1(it, it).fnc2(it, it);
var5: = ab.fnc1(ab).fnc2(ab.fnc1(ab));

And we can change the position of this argument in UFCS. In this example, this object is the last argument:

//  : = fnc1(arg, obj);
var1: = obj.fnc1(arg, it);

This feature improves chained UFCS without requiring to use additional helper function call and lambda expression. Considering the example from the proposal, it will be changed with UFCS and it keyword in Cpp2:

filter_out_html_tags_cpp2: (sv: std::string_view)
  sv.transform(: (e) e == '<' || e == '>')
//  .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()));)
    .zip_transform(std::logical_or(), it, scan_left(it, true, std::not_equal_to()))
    .zip(sv)
    .filter(: (t) ! t.get<0>())
    .values()
    .to<std::string>();

If it's hard to find it keyword (although an editor with syntax highlighting can help in this case), another syntax such as $$ may be used.

The point of the placeholder is that:

So I'm in favor of @bluetarpmedia's comment (but in a way that placeholder is optional):

You're right that UFCS is very close already, and arguably the dot syntax is just as nice as the new operator, but with UFCS I'd still prefer a token to make the syntax simpler. (Perhaps _ is better than Circle's choice of $ since there's already precedent in Cpp2.)

.zip_transform(std::logical_or{}, _, scan_left(_, true, std::not_equal_to{}))

msadeqhe commented 11 months ago

In another words, the placeholder is optional. If it keyword (or any other notation) is passed as an argument in UFCS, this argument will be passed in place of it:

var1: = ab.fnc1(mn, it);
//  : =    fnc1(mn, ab);

Otherwise this argument will be implicitly the first argument:

var1: = ab.fnc1(mn);
//  : =    fnc1(ab, mn);

So we can have chained function calls regardless of whether this is the first parameter or not.

msadeqhe commented 11 months ago

Optional Placeholder Syntax

If you find optional placeholders not to be readable, its readability may be increased by adding extra this like if it was a keyword argument (it depends on what will be the syntax of keyword arguments or designated constructors):

//  : = obj.fnc1(this: = x, it);
//  : = obj.fnc1(this? x, it);
var1: = obj.fnc1(this x, it);

Briefly A.F(this B, it) is equal to B.F(A) which it falls back to F(B, A). That's it.

Without this, we cannot write it keyword in argument list:

var1: = obj.fnc1(x, it); // ERROR! `it` keyword requires explicit `this` argument.

And without it, UFCS will work as how we currently use it:

var1: = obj.fnc1(x); // OK. It falls back to `fnc1(obj, x)`.

The example from the proposal will be like this (with new syntax):

filter_out_html_tags_cpp2: (sv: std::string_view)
  sv.transform(: (e) e == '<' || e == '>')
//  .call(:(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to()));)
    .zip_transform(this? std::logical_or(), it, scan_left(it, true, std::not_equal_to()))
    .zip(sv)
    .filter(: (t) ! t.get<0>())
    .values()
    .to<std::string>();

So optional placeholder doesn't conflict with the way UFCS works today, it complements it. Thanks.

SebastianTroy commented 11 months ago

Suggestion to use 'lhs' instead of 'it' as keyword for this proposal. It is clearer as to the meaning of the value, and I found myself substituting 'it' for 'left hand side' in my head whenever I read it

On 15 November 2023 10:36:41 Sadeq @.***> wrote:

Optional Placeholder Syntax

If you find optional placeholders not to be readable, it may increase readability by adding extra this like if it was a keyword argument (it depends on what will be the syntax of keyword arguments or designated constructors):

// : = obj.fnc1(this: = x, it); // : = obj.fnc1(this? x, it); var1: = obj.fnc1(this x, it);

Briefly A.F(this B, it) is equal to B.F(A) which it falls back to F(B, A). That's it. Thanks.

Without this, we cannot write it keyword in argument list:

var1: = obj.fnc1(x, it); // ERROR! it keyword requires explicit this argument.

And without it, UFCS will work as how we currently use it:

var1: = obj.fnc1(x); // OK. It falls back to fnc1(obj, x).

So the placeholder doesn't conflict with the way UFCS works today, it complements it.

— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/741#issuecomment-1812213602, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQMNOYQDBIQY5TWNEDTYESLLLAVCNFSM6AAAAAA5ZQQZ32VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMJSGIYTGNRQGI. You are receiving this because you commented.Message ID: @.***>

JohelEGP commented 11 months ago

. :(x) zip_transform(std::logical_or(), x, scan_left(x, true, std::not_equal_to())) ()

That's not valid grammar (https://cpp2.godbolt.org/z/9avjYh6sb):

Right, I like the call helper well enough that I'm waiting to see if there's really a need to write a new function expression in the middle of a postfix-expression... it's doable but is it needed?

You could say it's needed for #748, so we could write w: (f) :(x) x.(f$)(); and have x.(f$)() call (f$)(x).

There is another use case. In a member function of a type template, UFCS on a dependent base member requires explicit qualification (https://cpp2.godbolt.org/z/Mff1G9331):

u: @struct <T> type = {
  f: (_) true;
}

t: @struct <T> type = {
  this: u<T>;

  operator(): (this, r: std::span<const int>) -> _ = {
    for r do (e) {
      // OK.
      return this.f(e);

      // error: explicit qualification required to use member 'f' from dependent base class
      //   16 |       return CPP2_UFCS_0(f, e); 
      //      |                          ^
      return e.f();

      // error: expected unqualified-id
      //   21 |       return CPP2_UFCS_0(f, e.(*this)); 
      //      |                               ^
      return e.this.f();

      // OK.
      return e.u<T>::f();

      // What I would like to write:
      // return e.(this.f)();
      //  That translates to this:
      return (this.f)(e);
    }
  }
}

main: () = {
  r: std::vector = (0, 1, 2);
  assert(t<int>()(r));
}
JohelEGP commented 11 months ago

From https://github.com/hsutter/cppfront/issues/748#issuecomment-1826410769:

Also, can't do UFCS on a dependent base member (https://github.com/hsutter/cppfront/issues/741#issuecomment-1826408534):

For now, I can workaround this with using u<T>::f;, which makes return e.f(); well-formed: https://cpp2.godbolt.org/z/T5b5sxbYr.

From https://github.com/hsutter/cppfront/issues/748#issuecomment-1826466798:

Function object prvalue

I forgot to mention this other case where you first need to construct a callable.

Consider the call in the assert in the main above: t<int>()(r). t<int>() is the function object we want to call.

Let's rewrite it in UFCS order: r.t<int>()(). The function to call becomes t<int> instead. The second () applies to the result of the UFCS.

The call helper does help: r.call(t<int>()) (https://cpp2.godbolt.org/z/Mfc5TP8xz).

DerpMcDerp commented 8 months ago

The unary pipeline operator can be used for terse lambda syntax, i.e. |> foo($0, $2) == :(x, _, z) foo(x, z);