Skip to content

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

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

Open
bluetarpmedia opened this issue Oct 10, 2023 · 41 comments
Open

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

bluetarpmedia opened this issue Oct 10, 2023 · 41 comments

Comments

@bluetarpmedia
Copy link
Contributor

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
Copy link
Contributor

JohelEGP commented Oct 10, 2023

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
Copy link
Owner

hsutter commented Oct 10, 2023

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
Copy link
Contributor

JohelEGP commented Oct 10, 2023

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
Copy link
Contributor Author

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
Copy link
Owner

hsutter commented Oct 10, 2023

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
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

Do you have a test?

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

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 10, 2023

. :(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
Copy link
Owner

hsutter commented Oct 10, 2023

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
Copy link
Contributor Author

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
Copy link
Contributor

JohelEGP commented Oct 10, 2023

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
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>();

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 10, 2023

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
Copy link

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

@JohelEGP
Copy link
Contributor

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
Copy link
Contributor

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 #741 (comment), 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
Copy link
Contributor

@codereport FYI.

hsutter added a commit that referenced this issue Oct 12, 2023
The code in this comment should now parse correctly:

#741 (comment)
@hsutter
Copy link
Owner

hsutter commented Oct 12, 2023

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?

@hsutter
Copy link
Owner

hsutter commented Oct 12, 2023

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
Copy link
Contributor

JohelEGP commented Oct 12, 2023

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
Copy link
Owner

hsutter commented Oct 12, 2023

[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:

  • UFCS is even more useful than I thought. It isn't just good for enabling generic code (to be able to call functions whether they're members or nonmembers, which today we can only do with operators) and good for enabling IDEs (autocomplete), but it can help reduce the pressure to add special-purpose language features like |> and reduce pressure to use special-purpose library techniques like overloading |.
  • The brand-new very terse syntax has surprised me with how immediately useful it has been, including in this thread. It's a smallish change that really feels game-changing, because it shortens just enough of the ceremony of declaring the anonymous function so that it goes from feeling like "an ordinary function that has this expression body" to "an ordinary expression that we use as a function just by prefixing :(x)" (both are valid, but the latter feels powerful to me somehow).

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 12, 2023

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
Copy link

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
Copy link
Contributor

JohelEGP commented Oct 12, 2023

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Owner

hsutter commented Oct 20, 2023

. :(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
Copy link
Contributor

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
Copy link

SebastianTroy commented Nov 11, 2023 via email

@JohelEGP
Copy link
Contributor

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
Copy link
Contributor

. :(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
Copy link
Contributor

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
Copy link

msadeqhe commented Nov 15, 2023

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:

  • It allows us to explicitly specify the position of this argument in UFCS.
  • It can be used to refer to the left operand of any binary operation too.
  • It doesn't require additional helper function call and lambda expression.

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
Copy link

msadeqhe commented Nov 15, 2023

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
Copy link

msadeqhe commented Nov 15, 2023

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
Copy link

SebastianTroy commented Nov 15, 2023 via email

@JohelEGP
Copy link
Contributor

JohelEGP commented Nov 25, 2023

. :(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
Copy link
Contributor

JohelEGP commented Nov 25, 2023

From #748 (comment):

Also, can't do UFCS on a dependent base member (#741 (comment)):

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 #748 (comment):

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).

zaucy pushed a commit to zaucy/cppfront that referenced this issue Dec 5, 2023
The code in this comment should now parse correctly:

hsutter#741 (comment)
JohelEGP referenced this issue Dec 7, 2023
@DerpMcDerp
Copy link

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

JohelEGP referenced this issue Sep 26, 2024
* Add `..` syntax to select a member function only

With this commit, `x.f()` is still UFCS, but `x..f()` will now find only a member function.

For now I like that the default is UFCS, but as we get more experience I'm open to changing the default so that `.` is member selection and `..` is UFCS (which would be a breaking change, but a mechanical one).

Also: Remove the old version of `get_declaration_of` now that the new lookup seems stable.

* Remove some debug code

* Finish removing old g_d_o paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants