cppfront icon indicating copy to clipboard operation
cppfront copied to clipboard

[SUGGESTION] Add possibility of using lambda in is-statements (inspect and if statements)

Open filipsajdak opened this issue 2 years ago • 2 comments

In https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2392r0.pdf there is an example that uses lambda with is keyword.

cout << inspect i -> std::string {
  is in(1,2) = "1 or 2";
  is in(2,3) = "3";
  is _ = "something else";
};

or

if i is in(10,20) {
  cout << "I between 10 and 20" << endl;
}

I started to experiment with current implementation to achieve the same functionality and I came up with implementation that you can check here: https://godbolt.org/z/464PP7bW6 (working prototype).

Unfortunately lambda with the capture list cannot be argument of the template (only gcc supports that). So, I have created a wrapper that stores a lambda and the arguments that normally will go to the capture list.

#include <utility>
#include <tuple>
#include <iostream>
#include <vector>

constexpr auto less_than = [](auto value) {
    return cpp2::lambda_wrapper([](auto x, auto capture) { return x < capture;}, value);
};

constexpr auto in = [](auto min, auto max) {
    return cpp2::lambda_wrapper([](auto x, auto capture) { 
        return std::get<0>(capture) <= x && x <= std::get<1>(capture);
    }, std::pair(min,max));
};

constexpr auto empty = cpp2::lambda_wrapper([](auto x){
    return std::empty(x);
});

main: () -> int = {
    i := 15;

    if i is less_than(20) {
        std::cout << "less than 20" << std::endl;
    }

    if i is in(10,30) {
        std::cout << "i is between 10 and 30" << std::endl;
    }

    v : std::vector<int> = ();

    if v is empty {
        std::cout << "v is empty" << std::endl;
    }
}

lambda_wrapper shall use std::tuple to store arguments that shall go to capture list but std::tuple is not structural type and cannot be used in that context. There are possible workarounds (https://stackoverflow.com/questions/69194075/structural-tuple-like-wrapping-a-variadic-set-of-template-parameters-values-in) but I decided to propose something simple.

The current implementation can handle lambda that captures:

  • none argument - using std::monostate that is default second template argument type (lambda_wrapper([](auto x){/**/})),
  • one argument - (lambda_wrapper([](auto x, auto capture){/**/}, captured_value)),
  • two arguments - using std::pair (lambda_wrapper([](auto x, auto capture){/**/}, std::pair(/* captured two values */) )),

If more captured values will be needed we need to add tuple-like structure that will be structural type.

On the last push I have made possible using lambda_wrapper in inspect alternatives (by making id-expression ( expression-list ) an valid alternative syntax.

That makes this code works:

std::cout << inspect i -> std::string {
    is less_than(10) = "i less than 10";
    is in(11,20) = "i is between 11 and 20";
    is _ = "i is out of our interest";
} << std::endl;

filipsajdak avatar Oct 23 '22 01:10 filipsajdak

Ah... the change works in if statements but it is not working yet with inspect - the patch to cppfront is needed not to perceive the syntax as an error.

filipsajdak avatar Oct 23 '22 17:10 filipsajdak

On the last push I have added support for using id-expression ( expression-list ) as alternative in the inspect expression.

filipsajdak avatar Oct 23 '22 19:10 filipsajdak

I have noticed that my other PRs regarding using is() with multiple types/templates interfere with this PR and I need to update it.

Currently, I am not able to allow generic lambda use as I don't know how to write requires clause for lambda_wrapper that will match them. Now it matches types that matches:

template <typename F, typename Capture = std::monostate>
    requires (
        requires { &F::operator(); }
    )
struct lambda_wrapper;

What is currently possible is visible here:

#include <utility>
#include <tuple>
#include <iostream>
#include <vector>
#include <span>

constexpr auto less_then = [](int value) {
    return cpp2::lambda_wrapper([](int x, int capture) { return x < capture;}, value);
};

constexpr auto in = [](auto min, auto max) {
    return cpp2::lambda_wrapper([](int x, std::pair<int,int> capture) { 
        return std::get<0>(capture) <= x && x <= std::get<1>(capture);
    }, std::pair(min,max));
};

constexpr auto empty = cpp2::lambda_wrapper([](std::span<const int> x){
    return std::empty(x);
});

main: () -> int = {
    i := 15;

    if i is less_then(20) {
        std::cout << "less than 20" << std::endl;
    }

    if i is in(10,30) {
        std::cout << "i is between 10 and 30" << std::endl;
    }

    std::cout << inspect i -> std::string {
        is less_then(10) = "i less than 10";
        is in(11,20) = "i is between 11 and 20";
        is _ = "i is out of our interest";
    } << std::endl;

    v : std::vector<int> = ();

    if v is empty {
        std::cout << "v is empty" << std::endl;
    }
}

The feature is less generic but still useful.

filipsajdak avatar Nov 26 '22 21:11 filipsajdak

Rebase to main. Make is() function constexpr

filipsajdak avatar Nov 29 '22 21:11 filipsajdak

Might require https://github.com/hsutter/cppfront/pull/86 for some use cases.

filipsajdak avatar Nov 29 '22 21:11 filipsajdak

Might require https://github.com/hsutter/cppfront/pull/86 for some use cases.

Should these be merged? Should all the is/as PRs be merged (also #79, #106, #108)? (Not suggesting, just asking. As I look through them I'm trying to grok the overlap.)

hsutter avatar Dec 17 '22 16:12 hsutter

It could be merged. I was not sure if you would be willing to merge all functionalities.

filipsajdak avatar Dec 17 '22 18:12 filipsajdak

Currently, I am not able to allow generic lambda use as I don't know how to write requires clause for lambda_wrapper that will match them.

Can you give an example of what doesn't work?

JohelEGP avatar Dec 17 '22 18:12 JohelEGP

The goal was to have each functionality separate.

filipsajdak avatar Dec 17 '22 18:12 filipsajdak

NP - if they don't conflict I'll plan to look at them individually for now. Thx!

hsutter avatar Dec 17 '22 18:12 hsutter

@JohelEGP labda_wrapper requires

requires { &F::operator(); }

It means that the below code will work in the is statement:

constexpr auto in = [](auto min, auto max) {
    return cpp2::lambda_wrapper([](int x, std::pair<int,int> capture) { // <--- explicit type int
        return std::get<0>(capture) <= x && x <= std::get<1>(capture);
    }, std::pair(min,max));
};

But this will not:

constexpr auto in = [](auto min, auto max) {
    return cpp2::lambda_wrapper([](auto x, auto capture) { // <--- generic lambda
        return std::get<0>(capture) <= x && x <= std::get<1>(capture);
    }, std::pair(min,max));
};

filipsajdak avatar Dec 17 '22 22:12 filipsajdak

You could move the constraint to the point where you actually have the argument type X with which to call F. Then you could use std::invocable<lambda_wrapper<F, Capture>, X> if the call operator of lambda_wrapper and F are likewise appropriately constrained: https://compiler-explorer.com/z/s3cbfccnh.

JohelEGP avatar Dec 17 '22 22:12 JohelEGP

I will test that direction.

Unfortunately, std::tuple cannot be used. As I described in the PR description:

lambda_wrapper shall use std::tuple to store arguments that shall go to capture list but std::tuple is not structural type and cannot be used in that context. There are possible workarounds (https://stackoverflow.com/questions/69194075/structural-tuple-like-wrapping-a-variadic-set-of-template-parameters-values-in) but I decided to propose something simple.

filipsajdak avatar Dec 17 '22 23:12 filipsajdak

Thanks! Implemented as a separate commit along the lines discussed in #79.

hsutter avatar Dec 22 '22 19:12 hsutter

@hsutter, your change enables using lambda in inspect expressions but not in if statements. My solution worked for both cases ;)

#include <iostream>
#include <vector>

constexpr auto less_then = [](int value) {
    return [=](auto x) { return x < value;};
};

constexpr auto in = [](auto min, auto max) {
    return [=](auto x) { 
        return min <= x && x <= max;
    };
};

constexpr auto empty = [](auto&& x){
    return std::empty(x);
};

main: () -> int = {
    i := 15;

    std::cout << inspect i -> std::string {
        is (less_then(10)) = "i less than 10"; // works
        is (in(11,20)) = "i is between 11 and 20"; // works
        is _ = "i is out of our interest";
    } << std::endl;

    if i is (less_then(20)) { // not working
        std::cout << "less than 20" << std::endl;
    }

    if i is (in(10,30)) { // not working
        std::cout << "i is between 10 and 30" << std::endl;
    }

    v : std::vector<int> = ();

    if empty(v) { // works
        std::cout << "v is empty" << std::endl;
    }

    if v is (empty) { // not working
        std::cout << "v is empty" << std::endl;
    }
}

filipsajdak avatar Dec 22 '22 22:12 filipsajdak

Ah, you noticed that. :) That was next on my to-do list, and I just checked it in... the above code now works with commit 327ceebeccdc0619c12d03da69df7ecf0c622502 .

hsutter avatar Dec 23 '22 21:12 hsutter