capturing monads in lambda

483 views
Skip to first unread message

szollos...@gmail.com

unread,
Sep 7, 2016, 4:36:50 PM9/7/16
to ISO C++ Standard - Future Proposals
Hi,

While thinking about for..else, I realized that it (and other language constructs) could be greatly simplified had we had the chance to capture monads created by keywords like return, continue, break. Before proposing any syntax/schematics, I'd first ask you, how would you feel about this generally. The idea is something like:
[&return, break_=&break, break=...](){...}

Capturing a monad assumes that it is valid to write it in the calling context.

Note that capture is schematic, i.e., 'return' inside the lambda will destruct locals/parameters before performing caller's return; same holds for break and continue.

What do you think?

Thanks,
-lorro

edward...@mavensecurities.com

unread,
Sep 7, 2016, 6:00:27 PM9/7/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
I'm assuming that a lambda capturing a control flow keyword has to have return type void.

Still, what would this do?

int f() { auto&& l = [&return](int i){ return i; }; (l(5), puts("hello")); }


szollos...@gmail.com

unread,
Sep 8, 2016, 2:53:36 PM9/8/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, edward...@mavensecurities.com
Hi,


> I'm assuming that a lambda capturing a control flow keyword has to have return type void.
It's actually [[noreturn]] void if you do [&return]; if you do [return_=&return], then you can return to both points (with potentially different return types). Note that the lambda drives, by type of value passed to the given return monad, what context it can be called in (just as for generic lambdas, where return type is deduced from type passed to return). Thus it's no more a function or a functor: it's a monad.
Alternatively, you can look at it as a functor having implicit monadic parameters.
Note that it's not the same as throwing an exception. It's valid after leaving the scope that declares it, i.e., it doesn't capture 'current' return, but the idea of returning. Simulating this via exceptions would require every call to be in sg like a try{}catch(return_type r) { return r; } (not recommended), which is a perfect pessimization.

Re: the function:

int f() {
   
auto&& l = [&return](int i){
       
return i;
   
};
   
(l(5), puts("hello"));
}

It's unspecified if it puts hello if that's the question, but it does not bring nasal demons on you. It returns 5 on f(), so int j = f() is still valid.

Thanks,
-lorro

Edward Catmur

unread,
Sep 8, 2016, 8:06:10 PM9/8/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Thursday, 8 September 2016 19:53:36 UTC+1, szollos...@gmail.com wrote:
> Hi,
>
> > I'm assuming that a lambda capturing a control flow keyword has to have return type void.
> It's actually [[noreturn]] void if you do [&return]; if you do [return_=&return], then you can return to both points (with potentially different return types). Note that the lambda drives, by type of value passed to the given return monad, what context it can be called in (just as for generic lambdas, where return type is deduced from type passed to return). Thus it's no more a function or a functor: it's a monad.
> Alternatively, you can look at it as a functor having implicit monadic parameters.
> Note that it's not the same as throwing an exception. It's valid after leaving the scope that declares it, i.e., it doesn't capture 'current' return, but the idea of returning. Simulating this via exceptions would require every call to be in sg like a try{}catch(return_type r) { return r; } (not recommended), which is a perfect pessimization.

But it does unwind the stack like throwing an exception, right? So it can unwind multiple stack frames (as long as they're within the original capture point) and unwind temporaries within expressions around stack nesting points for function calls.

Do you have an example showing how it could be useful with the algorithms library e.g. for early return from accumulate?

Nicol Bolas

unread,
Sep 8, 2016, 11:39:42 PM9/8/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com

I think that I don't really understand what it's supposed to do.

My elementary-school understanding of "monad" comes entirely from skimming Eric Lippert's series on them. And I'm not sure how "keywords like return, continue, break" constitute monads, nor do I understand what it means to "capture" them.

Can you explain what this feature is supposed to do in more basic terms of what it will do?

szollos...@gmail.com

unread,
Sep 9, 2016, 6:32:25 PM9/9/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Let's put aside calling them monads for a while, we'll return to that later. First some examples:

1. Error codes when you have no exceptions (due to embedded system / manager's wisdom / using vast amount of C libraries that return error codes). Here we assume that bool(ErrorCode) == true iff there was an error.

ErrorCode save()
{
   
// note that default exit is not the same as return here
   
auto return_if = [&return](auto retval){
       
if (retval) { return retval; }
   
};

    return_if
(openFile());
    return_if
(saveData());
    return_if
(closeFile());
}



2. This can be generalized to std::variant<ReturnType, ErrorCode>.

std::variant<Record, ErrorCode> load()
{
   
auto pass_error = [error = &return](auto ret){
       
// error looks like a [[noreturn]] function here
       
if (ret.index())
            error
(std::get<ErrorCode>(ret));

       
return std::get<0>(ret);
   
};
   
auto head = pass_error(loadHead());
   
auto tail = pass_error(loadTail());
   
return head + tail;
}



3. For the case of looping. Yes, I know this could be optimized.

for (auto&& elem : myVector)
{
   
auto onBreak = [&break](){ std::cout << "Match: " << elem; break; }

   
if( isPrime(elem) )
        onBreak
();
   
// ....
   
if( isSquare(elem) )
        onBreak
();

   
// not sure if we want to allow this:
   
//[&, break = &onBreak]{
   
//    if( isFibonacci(elem) )
   
//        break;
   
//}();
}



4. For many people want to break and continue n times from a nested loop.

for (auto&& person : people)
{
   
auto break_twice = [&break]{ break; }
   
for (auto&& phone : person.phones)
   
{
       
if (isLandline(phone))
       
{
            std
::cout << person.name << " " << phone
                     
<< " still uses landline, do not switch it off!"
                     
<< std::endl;
            break_twice
();
       
}
   
}
}


5. And, while we cannot have range for else with this, we can simulate it like: (note the difference between capture by reference and by value)

template<typename Range, typename Body, typename Else>
void for_or(const Range& r, const Body& b, Else e)
{
   
auto&& it    = r.begin();
   
auto&& itEnd = r.end();
   
if (it != itEnd)
   
{
       
do
       
{
            b
(*it);
       
} while (++it != itEnd);
   
}
   
else
   
{
        e
();
   
}
}

template<typename CPlugin>
bool initPlugins(const CPlugin& plugins)
{
    for_or
(plugins, [=return, &break, &continue, &](const Plugin& plugin) {
       
// this means we have initPlugin's return and calling point's break, continue
       
if (!plugin.requires_init())
           
continue;
       
if (!plugin.init())
           
return false;
   
}, [&return] {
       
return DefaultPlugin::initialize();
   
}
   
return true;
}



-----

And now, the 'monadic benefits' :). You have seen that this essentially superior to for-else and for-break and break(n), albeit with a clumsy syntax. Which you can make a macro for.
Also, you might think of `{ ... }` code blocks as `[=return, &break, &continue, &]{ ... }();`, add `this` if defined. The benefit of this is that now `if .. [else]`, `for`, `do .. while`, `while`, etc. are all monads, working on a lambda (e.g. a for-loop lifts T -> void to Range<T> -> void and calls it). This introduces the syntax `(type var : rval)` that passes rval to the monad on the left and is the parameter declaration of a lambda being declared in-place. If we now allow user-defined monads, we can have `for_or(auto&& x : v) { ... } for_else { ... }`. If we allow decoupling of `(type var : rval)`, we have the `for` monad in C+: `auto initAll = for (auto&& x) { x.init(); }; initAll(plugins.queue());`. These two ifs are strictly optional :).

Thanks,
-lorro

Nicol Bolas

unread,
Sep 9, 2016, 11:46:42 PM9/9/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Friday, September 9, 2016 at 6:32:25 PM UTC-4, szollos...@gmail.com wrote:
Hi,

Let's put aside calling them monads for a while, we'll return to that later. First some examples:

1. Error codes when you have no exceptions (due to embedded system / manager's wisdom / using vast amount of C libraries that return error codes). Here we assume that bool(ErrorCode) == true iff there was an error.

ErrorCode save()
{
   
// note that default exit is not the same as return here
   
auto return_if = [&return](auto retval){
       
if (retval) { return retval; }
   
};

    return_if
(openFile());
    return_if
(saveData());
    return_if
(closeFile());
}


... So what does this do?

See, it sounds like you're trying to say that returning from within the lambda will cause it to return from... well, that right there is the question. I think you want it to return from the function that called it. But how exactly is that supposed to happen?

After all, the function that called it is not necessarily the function that created it. What happens if you do this:

ErrorCode save()
{
    std
::function<void(ErrorCode)> return_if = [&return](auto retval){

       
if (retval) { return retval; }
   
};

    return_if
(openFile());
    return_if
(saveData());
    return_if
(closeFile());
}

I have every reason to expect this to achieve the same effect. Yet I cannot imagine how that would be possible. After all, I don't call it. I call `std::function::operator()`, which (eventually) calls the lambda. So the lambda will just provoke the return of some construct within the `std::function::operator()`.

And you can't say that it will always return to the caller of the lambda's creator, because I don't have to still be on the stack. I can return that lambda to someone else, wrapped in a `std::function`. How does that work?

A lambda in C++ is not some magical construct, imbued with fantastical powers. In C++, a lambda is an object. Not only that, it is an instance of a class type. You can rewrite any lambda as a local struct with a constructor and an operator() overload (except for generic lambdas, but I'd like to see that fixed). As it currently stands, a lambda cannot do anything that a user-created class can't also do.

And that is good. Lambdas are syntactic sugar. Very useful and sweet sugar, but sugar never-the-less. It is good that C++ defines lambdas as compiler-created class types that follow all of the rules of such types. It makes the language more regular and ensures the sanity of both lambdas and of the type system.

The rules of C++ functions do not allow a function call to directly affect the control flow of the function that called it. Well, actually it does allow that; we call that an exception.

What you want is to write some pattern of code that can be used repeatedly on arbitrary inputs, which will act on its caller outside of the normal rules of C++ functions. That the code will act as if it were actually copied into its caller, rather than following the rules of calling a function. Like a macro, only... well actually, exactly like a macro.

A C++ function, as it currently stands, cannot do that. So either you want to create a new construct with its own rules, or you want to expand the rules of all functions to include such functionality.

But we should not abuse lambdas by imbuing them with magical powers that regular C++ function and types cannot achieve. If you want to give lambdas these powers, then it needs to be done in a way that gives other functions and types these powers too.

If you want control structures in C++ to be "monads" or whatever, then re-define them that way. But don't try to pervert lambdas into some tool to achieve that effect.

-----

And now, the 'monadic benefits' :). You have seen that this essentially superior to for-else and for-break and break(n), albeit with a clumsy syntax.

I have seen no such thing. I find it very difficult to agree with the statement that some solution is "essentially superior" to an alternative, when that solution is essentially "wrap all your code in a bunch of lambdas."

Since before C++11 hit, I've seen lambdas abused for all kinds of stuff that they should never be used for. Designing language features that encourage such abuse is not a good thing.

Which you can make a macro for.

"Just wrap it in a macro" is not a valid justification for an intrinsically hideous language feature. Especially if you're designing a language feature that actually could have avoided being Medusa-levels of ugly if the feature had been designed properly.

Edward Catmur

unread,
Sep 10, 2016, 7:23:50 PM9/10/16
to std-pr...@isocpp.org, szollos...@gmail.com

On 10 Sep 2016 04:46, "Nicol Bolas" <jmck...@gmail.com> wrote:
> See, it sounds like you're trying to say that returning from within the lambda will cause it to return from... well, that right there is the question. I think you want it to return from the function that called it. But how exactly is that supposed to happen?

Stack unwinding.

> After all, the function that called it is not necessarily the function that created it. What happens if you do this:
>
> ErrorCode save()
> {
>     std::function<void(ErrorCode)> return_if = [&return](auto retval){
>
>         if (retval) { return retval; }
>     };
>
>     return_if(openFile());
>     return_if(saveData());
>     return_if(closeFile());
> }
>
> I have every reason to expect this to achieve the same effect. Yet I cannot imagine how that would be possible. After all, I don't call it. I call `std::function::operator()`, which (eventually) calls the lambda. So the lambda will just provoke the return of some construct within the `std::function::operator()`.

Stack unwinding isn't just for exceptions. The machinery is there for non local jumps, such as thread cancellation, setjmp and SEH.

> And you can't say that it will always return to the caller of the lambda's creator, because I don't have to still be on the stack. I can return that lambda to someone else, wrapped in a `std::function`. How does that work?

UB, just as with capturing local variables by reference. Likewise if any of the intervening frames are not unwinding aware.

> A lambda in C++ is not some magical construct, imbued with fantastical powers. In C++, a lambda is an object. Not only that, it is an instance of a class type. You can rewrite any lambda as a local struct with a constructor and an operator() overload (except for generic lambdas, but I'd like to see that fixed). As it currently stands, a lambda cannot do anything that a user-created class can't also do.

There's another feature you're forgetting. Lambda captures can copy arrays by value, which user code can't do without some heavy library machinery.

If we're talking user code equivalents, setjmp would do if it's unwind-enabled, as would a custom exception in the absence of catch (...) blocks, or even invoking the platform unwind machinery directly.

> <snip>

Sure, I get that you like that lambdas are mostly syntactic sugar. That was probably necessary at the beginning, but it doesn't mean they have to stay that way.

Nicol Bolas

unread,
Sep 11, 2016, 12:42:08 PM9/11/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Saturday, September 10, 2016 at 7:23:50 PM UTC-4, Edward Catmur wrote:

On 10 Sep 2016 04:46, "Nicol Bolas" <jmck...@gmail.com> wrote:
> See, it sounds like you're trying to say that returning from within the lambda will cause it to return from... well, that right there is the question. I think you want it to return from the function that called it. But how exactly is that supposed to happen?

Stack unwinding.

> After all, the function that called it is not necessarily the function that created it. What happens if you do this:
>
> ErrorCode save()
> {
>     std::function<void(ErrorCode)> return_if = [&return](auto retval){
>
>         if (retval) { return retval; }
>     };
>
>     return_if(openFile());
>     return_if(saveData());
>     return_if(closeFile());
> }
>
> I have every reason to expect this to achieve the same effect. Yet I cannot imagine how that would be possible. After all, I don't call it. I call `std::function::operator()`, which (eventually) calls the lambda. So the lambda will just provoke the return of some construct within the `std::function::operator()`.

Stack unwinding isn't just for exceptions. The machinery is there for non local jumps, such as thread cancellation, setjmp and SEH.


My first inclination is to say that this is just exception handling with special cleanup work. Conceptually, it's indistinguishable from:

auto x = []() {... throw return_value(someValue);} //Class template deduction

try
{
 
auto y = x(foo);
}
catch(return_value<ReturnType> &t)
{
 
return std::move(t.get());
}

Obviously that's both incredibly verbose and traps `y` within a try-block. But the general idea you seem to want is the same; it's simply a matter of applying syntactic sugar:

auto y = try x(foo);

Or something to that effect, with the compiler generating logic based on certain types being thrown. `std::return_value<T>` would provoke a return from that function of the stored `T`. `std::break_value` would provoke a `break`. If nothing is thrown, then the code proceeds as normal.

The `try` there is important for two reasons. First, it lets the compiler know where to stop looking. And second, it makes clear to the reader that the code in question may experience an alternate control flow. Just as with any other `try` block.

A lambda does not seem to be the natural form of syntax for applying this. If you're going to start doing stack unwinding and such, then it should be built on the language mechanism that actually does stack unwinding.

Consider `expected<T, E>`. By decoupling this feature from lambdas, by giving all functions this power, we could give it an implicit `operator T` overload that will either return a `T` or throw a return of `E`. So if you do `auto x = try Typename(exp);`, you get automatic unpacking or a return of the error code.

Granted, some might suggest that this is no less dangerous or performance-costly than exceptions. To which I answer... yeah. That's what happens when you want code to be able to transparently communicate with some other code that is an indeterminate number of function calls below it.
 

> And you can't say that it will always return to the caller of the lambda's creator, because I don't have to still be on the stack. I can return that lambda to someone else, wrapped in a `std::function`. How does that work?

UB, just as with capturing local variables by reference. Likewise if any of the intervening frames are not unwinding aware.


The last example the OP posted makes a distinction between "capturing" by value vs. by reference. By reference `return` means causing a return from the most recent caller. By value `return` means causing a return from the code that created it.

So really, the OP's proposal is kinda backwards from what you seem to be wanting (and to be honest, I rather prefer your interpretation). "Capturing" statements by reference is *safe* and such lambdas can be used anywhere (in theory), while "capturing" by value is unsafe.

> A lambda in C++ is not some magical construct, imbued with fantastical powers. In C++, a lambda is an object. Not only that, it is an instance of a class type. You can rewrite any lambda as a local struct with a constructor and an operator() overload (except for generic lambdas, but I'd like to see that fixed). As it currently stands, a lambda cannot do anything that a user-created class can't also do.

There's another feature you're forgetting. Lambda captures can copy arrays by value, which user code can't do without some heavy library machinery


It doesn't take "heavy library machinery" to copy an array. Well, so long as the type is default-constructible and assignable.

Ultimately, the point I'm making is that a lambda is not a special thing; it doesn't do anything you can't do yourself, even if doing it yourself is much harder. What's being proposed here is a deviation from that: you cannot replicate such behavior.

And that ultimately limits the utility of the proposal.

If we're talking user code equivalents, setjmp would do if it's unwind-enabled, as would a custom exception in the absence of catch (...) blocks, or even invoking the platform unwind machinery directly.

> <snip>

Sure, I get that you like that lambdas are mostly syntactic sugar. That was probably necessary at the beginning, but it doesn't mean they have to stay that way.


That's not a good enough reason to tie it to lambdas.

Consider two of the examples provided here: `return_if` and `error_code`. Those patterns are quite useful for many  circumstances. And yet, there is no way to avoid re-typing them every time you want to use them without employing a macro. Why?

Because the feature is tied to lambdas and their capture list. Remove that tie, and it suddenly becomes quite possible to have these as simply standard library template functions.

Robert Bielik

unread,
Sep 11, 2016, 12:55:19 PM9/11/16
to std-pr...@isocpp.org, szollos...@gmail.com

Been following this thread, and I just want to chip in with a very important aspect: Readability. In ten cases of ten, I'd use exceptions as a mechanism to achieve the same behavior. Why? Because it's more readable, and not obfuscated.


--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/dcb70287-4ee4-4cae-9e09-4f7cab8d0db8%40isocpp.org.

szollos...@gmail.com

unread,
Sep 12, 2016, 3:03:01 AM9/12/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Thanks for the comment from both of you, it's very helpful and gives me hints on what to rephrase to make it easier to understand. Please bear with me, I hope I can make it clearer :).

First of all, I'm not planning it for lambdas only - it's just the best use case for me. Also, it helps me bring multiple language features to a simpler one. I do encourage making `return` capture available to functions, functors, etc.. - but I still have the for-loop glasses on :).

> std::function<void(ErrorCode)> return_if = [&return](auto retval){ ... };
So, let's distinguish two use cases: (NB. I use 'stack' as a synonym of scoped variables, not as a data structure)
  • [=return], by value capture, should provide a 'noreturn functor' (terminology to be cleared up) that, when called, will unwind the stack to the point we defined it, then perform the 'return' of that point. Therefore, the type it should accept is the return type of the function defining it. As Edward wrote, it's essentially the same as setjmp() with return / longjmp() in an object-oriented way.
    • It can only be defined in a function. If the capturing function has returned, it's UB to call it.
    • The above implies that it's UB to call it twice.
    • It can be simulated by a `try { ... } catch(NonLocalReturn<type>& r) { return r.get(); }` block at the defining point; however, it can be done more efficiently.
    • Parameter type of the returning functor is defined by the defining context - this can be checked compile-time.
  • [&return], by reference capture might have a different meaning in each point of execution. At the calling point (thus not at the point of definition) it should provide a 'noreturn fuctor' that, when executed, will unwind the stack to the point we called it, then perform the 'return' of that point. Therefore, the returning functor provided can only be called with the return type of the caller. It can be thought of an additional parameter to the function/functor/lambda - whether that needs to be templated is up to discussion.
    • There's no restriction on where to define and call it. E.g., it's well-defined to call it after the defining function.
    • Calling the returning functor twice (e.g. by passing it as a parameter of the first call of it) is well-defined.
    • It can be simulated by a `try { ... } catch(NonLocalReturn<type>& r) { return r.get(); }` block at each calling point; however, it can be done more efficiently.
    • Parameter type of the returning functor is defined by the calling context - this can be checked compile-time at each point of call. Note that these types might be different (conversion rules apply).
  • break and continue:
    • [=break], [=continue] are defined as [=return], except for
      • these can only be defined in a context where break and continue are valid;
    • [&break], [&continue] are defined as [&return], except for
      • these can only be called in a context where break and continue are valid;
    • Except for, for all the four:
      • breaking and continuing functors accept no parameters
      • these perform break and continue instead of return.

This implies that [&return] is invalid for std::function<void(ErrorCode)>, while [=return] should do what you expect it to do. It also implies, if we use careful wording, that [&return] is available for functions and member functions - either by this capture, or the more convenient parameter syntax (e.g.,`int f(int a, T ret = &return) { ... }`).


However, I'd go one step further. We can now think of code blocks, lambdas and bodies of for-loops in a unified manner. I'm not saying to rewrite compilers to this style, but it simplifies understanding for me. E.g. a body {} of a for-loop (that's repeated by `for`)is nothing different from a lambda that captures &return, redefines break and continue; and a `for` becomes a transformation on the lambda. These transformations, if we wish, can be made available to the user at no additional cost.


Note that the complex checks that exception handling is built upon is unnecessary for this return logic. An exception must check each catch()-block in reverse of the calling order and check if there's a type match. For these captures, there's one type accepted and one point to return to (per captured item), thus no checks for each stack frame is necessary. It's also way more strict, providing compile-time errors if you pass an incorrect type (rather than runtime issues that you get with uncaught exceptions). The second is why I'd use this rather than exceptions.


As for the example with accumulate, `[return_accumulated=return](double a, double b){ if(b==0) return_accumulated(0); return a*b; }` is a trivial optimization.


Macros and syntax: I'm not committed to any specific syntax yet. The one I've listed above is indeed verbose, it's simply to help discuss the scope and functionality. At a later step we might refine syntax, simplify base cases, clean it up, etc. Right now what I'd like to see is if we could cover the usual for-extensions, non-local return/break/continue and similar, frequently asked feature requests - and if yes, could we unify the syntax a bit..


Thanks again for your help on this,

-lorro

D. B.

unread,
Sep 12, 2016, 3:28:27 AM9/12/16
to std-pr...@isocpp.org
I can't help but feel that the chosen capturing methods of &/= for return-from-definer vs -caller respectively are arbitrary and confusing. Certainly they have no parity with current variable captures, while stealing syntax from them.. In fact, to me, it would make more sense for return-from-definer to be captured as &return (opposite to what you have now) and return-from-caller to be captured as an additional argument to the lambda function call.

But that assumes you want to, or that it's possible to, implement this while maintaining any intuitive degree of consistency with current usage. I'm not sure that's possible.

but oh, hey - maybe you could instead distinguish between the two possible points of return/break/whatever by hijacking static and extern instead! so capturing "extern return" would mean 'return here, regardless of where you're called', but capturing "static return" would mean 'return from the cnotext in whcih you're called'. Is that a decent or terrible idea? I can't tell.

D. B.

unread,
Sep 12, 2016, 3:32:38 AM9/12/16
to std-pr...@isocpp.org
Gah. Aside from the numerous typos, of course I got the symbols here the wrong way around, so that first line should read:

"I can't help but feel that the chosen capturing methods of =/& for return-from-definer vs -caller respectively are arbitrary and confusing."

I also see that you did say that from-caller &return can be thought of as an extra parameter, so again, not phrasing it as one is counterintuitive. But then sandwiching it into the args might also have issues. Either way,you're right that it might make sense to explicitly template it.



szollos...@gmail.com

unread,
Sep 12, 2016, 3:31:47 PM9/12/16
to ISO C++ Standard - Future Proposals
Hi,

I'm happy with whatever syntax we come up with. Note that I'm not intending to hijack lambda capture syntax, I'm saying that `auto myReturn = static return;` and `auto myReturn = extern return;` are also valid e.g. in functions. These will produce [[noreturn]] functors (one of them having multiple overloads/templates), but also provide diagnostics if incorrect type is used. What do you think?

Thanks,
-lorro

szollos...@gmail.com

unread,
Sep 12, 2016, 4:25:09 PM9/12/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
P.S. how about return const (to the capture point's caller) and return mutable or volatile (to the current caller)?

Arthur O'Dwyer

unread,
Sep 12, 2016, 8:58:00 PM9/12/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Rather than bikeshedding the syntax, I think you need to take a step back and explain what this proposed feature is. Right now, it reads like one of those joke programming languages like INTERCAL or TURKEY BOMB where words are just thrown together for comedic effect, rather than for intrinsic meaning (let alone usefulness).

What would it mean for a function to "break, but in its caller's context"?
You need to show a specific example of the semantics you're trying to achieve, in the form of
- a library solution (perhaps involving setjmp/longjmp or exception-handling under the hood), and/or
- assembly code for a non-trivial use of the proposed feature.

You get close to that with your statement
> It can be simulated by a `try { ... } catch(NonLocalReturn<type>& r) { return r.get(); }` block at each calling point; however, it can be done more efficiently.

but you need to explain how it can be done more efficiently; and besides,
- that only helps for the "return from local caller" case, not "return from original creator";
- that only has an obvious translation if type is known beforehand;
- the obvious translation is to wrap literally every function call in this boilerplate (since we can't tell which functors might or might not have these new semantics).

If you can't explain the practical semantics of your proposal, then you're operating solidly in joke-language territory: "Hey, I think C++ should have a COME FROM statement!"  "I think instead of BITs we should use AMICEDs, which are negative six-sevenths of a decimal digit!"  All this bikeshedding about the spelling of the COME FROM statement is completely beside the point.

–Arthur

Giovanni Piero Deretta

unread,
Sep 13, 2016, 5:18:58 AM9/13/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Tuesday, September 13, 2016 at 1:58:00 AM UTC+1, Arthur O'Dwyer wrote:
Rather than bikeshedding the syntax, I think you need to take a step back and explain what this proposed feature is. Right now, it reads like one of those joke programming languages like INTERCAL or TURKEY BOMB where words are just thrown together for comedic effect, rather than for intrinsic meaning (let alone usefulness).

What would it mean for a function to "break, but in its caller's context"?
You need to show a specific example of the semantics you're trying to achieve, in the form of
- a library solution (perhaps involving setjmp/longjmp or exception-handling under the hood), and/or
- assembly code for a non-trivial use of the proposed feature.


I think that the proposal is definitely confused and mentioning monads doesn't help. What the author is proposing is simply capture of first class continuations. The proposed syntax only allow capturing some specific continuations (the return, and loop and switch break continuations) and only allow escaping to them (by throwing away the current continuation), but it could be extended to capture any continuation and preserving the current one. The proposal is semantically a subset of the existing stackfull coroutine proposals and can be implemented on top of them.

Tony V E

unread,
Sep 13, 2016, 10:35:13 AM9/13/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
COME FROM is also how you can add threads to INTERCAL.
PLEASE. 


Sent from my BlackBerry portable Babbage Device
From: Arthur O'Dwyer
Sent: Monday, September 12, 2016 8:58 PM
To: ISO C++ Standard - Future Proposals
Subject: Re: [std-proposals] Re: capturing monads in lambda

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

Avi Kivity

unread,
Sep 14, 2016, 6:04:31 AM9/14/16
to std-pr...@isocpp.org, szollos...@gmail.com

On 09/11/2016 07:55 PM, Robert Bielik wrote:

Been following this thread, and I just want to chip in with a very important aspect: Readability. In ten cases of ten, I'd use exceptions as a mechanism to achieve the same behavior. Why? Because it's more readable, and not obfuscated.



Exceptions are too slow for this use case.

To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.

To post to this group, send email to std-pr...@isocpp.org.

szollos...@gmail.com

unread,
Sep 21, 2016, 6:21:53 PM9/21/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Sorry if it's confusing.. So far, it's just a sketch that I'm cleaning up based on others' suggestions. I'm happy to see the coroutines proposal. Initially, I've discarded the idea of describing these captures as I thought that full continuation capture is way to slow for this (in fact, this should be exactly as fast as loops and function returns); but you're right, let's formulate these based on continuations, then let's see optimization possibilities later. (Btw, as for the subject: is it OK to rename (set new subject) a thread on this list?)
So, what I'm proposing is absolute and relative capture of specific continuations, namely the ones called by return, break, continue. All of these currently throw away current continuation and it should remain that way. Absolute capture, when requested, should return a [[noreturn]] functor that is identical, both in parameter(s, if any) and effect to performing the same return, break, continue in the capturing context. Current continuation is thrown away and destruction of any automatic storage duration variables are performed in the usual (reverse-of-declaration) order (i.e., unwinding is performed), before calling the continuation, up to the capture point. It is UB to call the functor after leaving capture context or from another thread (or coroutine).
Relative capture, when requested, should return a [[noreturn]] functor that is identical, both in parameter(s, if any) and effect to performing the same return, break, continue in the calling context. This can either be supported by the language; or a workaround is possible via having an additional function parameter that has a default value: (as default value is evaluated in caller's context)

#define TAKE_RETURN /* syntax to be defined */

template<typename T>
int mul_for_accumulate(int a, int b, T return2 = TAKE_RETURN)
{
   
if (a==0 || b==0) return2(0);
   
return a * b;
}



If language support is provided, it's only available in functions; and in this case, it's UB to call relative return functor with a parameter that not convertible to the caller's return type; similarly, it's UB to call break functor or continue functor if these are not available in the caller's context. Without language support we're more type-safe, but the caller is aware of the capture (and can divert it); with language support it's easier to run into UB but it's meaning is always the same.

A possible 4th continuation to capture would be backtrack (or restart or cc), that captures the current continuation. It's similar to setjmp()/longjmp(), but performes unwinding and can be called multiple times until leaving the context. If not provided, it can be emulated by while() and continue+break:

while(true)
{
   
auto my_backtrack = TAKE_CONTINUE;
    f
(..., my_backtrack);
   
break;
}

Alternatively, loops can be defined in terms of backtrack.

Note that, up to this point, we only needed an unwind mechanism (that, as noted, is already there for exceptions) and a local goto-like operation (break and continue can be mechanically translated to it).

It looks preferable to be able to 'rename' return, continue and break (albeit looks can be deceiving, any thoughts on that are welcome).

The 'when you've got a hammer, everything looks like a nail'-observation is that now we can decompose 'for'. If we define:

for ( int i : range ) { ... }

as

for ([&, return = TAKE_RETURN]
     
(int i, auto continue = TAKE_CONTINUE, auto break = TAKE_BREAK) { ... }
   
)(range)

, then it's easy to define a 'for-function' that provides loop logic. While we might not want to redefine for itself this way, we might use the above syntax for abitrary functions, given we define the above substitution. This would provide immediately the functional equivalent of 'for break', with some library support, it'd do for 'for else'; as a side effect, it'd allow to separate the loop object from it's execution, thereby allowing to store the loop object and call it for different ranges.

Thanks for the valuable comments and the time,
-lorro

Avi Kivity

unread,
Sep 22, 2016, 12:30:26 PM9/22/16
to std-pr...@isocpp.org, szollos...@gmail.com

This could be combined with something I've been thinking about: a macro facility.


Motivating example:


    template <typename... Args>

    void log(const char* fmt, Args&&... args) {

        if (logging_enabled) {

            do_log(fmt, std::forward<Args>(args)...);

        }

    }


Now, the problem with this is that if an arg is expensive to compute, we take the cost before the if (logging_enabled) check.


What we could have instead is define log as a macro.  Each arg would then not be captured as a reference, but as a lambda that yields the argument:


    template <typename... Args>

    macro void log(const char* fmt, Args&&... args) {

        if (logging_enabled) {

            do_log(fmt, args()...);

        }

    }


An argument x in log("%s", x) would be translated as [&] () { return x; } -> decltype(x).  Combined with capturing return/break/continue etc. we could introduce new syntax:


    with_lock(my_lock) {

        do things that need my_lock held

        if (some_condition) return;  // releases my_lock and returns from enclosing scope

        do more things

    }


of course, need some syntax to capture the compound statement as a lambda; Swift has support for that we could borrow.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

szollos...@gmail.com

unread,
Sep 26, 2016, 6:20:34 PM9/26/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

I was about to say that it's a different issue, but it's actually not. I'm, however, not convinced that we need a new keyword for this. What you want basically is capturing an expression rather than its value as a function parameter. This facility already exists in ?:, ||, &&, we just need to extend that for regular functions. Something like:

template<typename T>
T my_ternary
(const bool cond, ->T true_t, ->T false_t)
{
   
if (cond)
       
return true_t();
   
return false_t();
}

int main()
{
   
int i = 0;
   
auto a = [&]() mutable { return ++i; };
    auto b = [&]() mutable { return ++i; };
    std
::cout << my_ternary(rand()%2, a(), b()); // should print 1, no UB/IDV
}

That way you can choose which params you take by value and which as expression. Now, a lot of questions arise which would deserve a specific thread on that.

Thanks,
-lorro

szollos...@gmail.com

unread,
Oct 9, 2016, 12:58:57 PM10/9/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

A toy model of the capture part is possible using gcc's statement expressions extension. This is not for implementation reference, just to have a clearer view of the idea:

#include <vector>
#include <iostream>
#include <experimental/optional>
#include <cmath>
#include <limits>

enum class return_mode { ret, brk, cont, back };
enum { parent_break };
enum { parent_continue };
enum { to_parent };

// TODO: specialize for void
template<typename R, typename P>
struct extended_return
{
   
const return_mode                    which_;
   
const std::experimental::optional<P> parent_retval_;
   
const std::experimental::optional<R> retval_;
    extended_return
(R retval)
       
: which_ (return_mode::back)
       
, retval_(std::move(retval)) {}
    extended_return
(decltype(to_parent), P retval)
       
: which_ (return_mode::ret)
       
, parent_retval_(std::move(retval)) {}
    extended_return
(decltype(parent_break) tgt)
       
: which_ (return_mode::brk) {}
    extended_return
(decltype(parent_continue) tgt)
       
: which_ (return_mode::cont) {}
   
operator return_mode() const { return which_; }
};

extended_return
<double, double> mul2(double lhs, double rhs)
{
   
if (rhs == 0) {
       
return { to_parent, 0.0 };
   
}
   
return lhs * rhs;
}

double mul2(double lhs, double rhs, void(*parent_return)(double))
{
   
if (rhs == 0) {
        parent_return
(0.0);
   
}
   
return lhs * rhs;
}

#define take_return                           \
           
else if(ret == return_mode::ret)  \
               
return *ret.parent_retval_;

#define take_break                            \
           
else if(ret == return_mode::brk)  \
               
break;

#define take_continue                         \
           
else if(ret == return_mode::cont) \
               
continue;

#define invoke_with(TAKEN, F, ...)            \
       
({                                    \
           
auto ret = F(__VA_ARGS__);        \
           
if (false);                       \
            TAKEN                            
\
           
*ret.retval_;                     \
       
});


#define invoke_with_abs(R, F, ...)            \
       
({                                    \
           
struct parent_return_t            \
           
{                                 \
                R value
;                      \
           
};                                \
           
auto parent_return = +[](R v) {   \
               
throw parent_return_t{v};     \
           
};                                \
            std
::experimental::optional<      \
               
decltype(F(__VA_ARGS__,       \
                           parent_return
))    \
           
> ret;                            \
           
try {                             \
                ret
= F(__VA_ARGS__,          \
                        parent_return
);       \
           
} catch (parent_return_t r) {     \
               
return std::move(r.value);    \
           
} catch (...) {                   \
               
throw;                        \
           
}                                 \
           
*ret;                             \
       
});

template<typename R>
double prod(const R& r)
{
   
if (r.empty()) {
       
return NAN;
   
}
   
double p = 1.0;
   
for (auto ri : r) {
        p
= invoke_with(take_return, mul2, p, ri);
       
//p = invoke_with_abs(double, mul2, p, ri);
   
}
   
return p;
}

int main()
{
    std
::vector<int> v{1, 2, 0, 4};
    std
::cout << prod(v);
}


Thanks,
-lorro

szollos...@gmail.com

unread,
Oct 15, 2016, 1:25:37 PM10/15/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

On another thread, `expr` was proposed for a lambda-like construct that's forwarding as-is (i.e., no need for std::forward<T>(expr)). We might use that syntax for passing return, break and continue:

template<typename T>
void g(int i, T brk)
{
    std
:cout << i;
   
if (i % 2) brk();
}

bool f(std::vector<int> v)
{
   
for (auto&& i : v) {
        g
(i, []`break;`);
   
}
}



What do you think?

Thanks,
-lorro

Tony V E

unread,
Oct 15, 2016, 1:38:00 PM10/15/16
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
To me, that syntax is understandable just by looking at it.

All the other syntax up to now was just confusing.

Sent from my BlackBerry portable Babbage Device
Sent: Saturday, October 15, 2016 1:25 PM
To: ISO C++ Standard - Future Proposals
Subject: Re: [std-proposals] Re: capturing monads in lambda
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Bjorn Reese

unread,
Oct 16, 2016, 8:18:09 AM10/16/16
to std-pr...@isocpp.org
On 10/15/2016 07:25 PM, szollos...@gmail.com wrote:

> g(i,[]`break;`);

or g(i, inline [] { break; });

szollos...@gmail.com

unread,
Oct 18, 2016, 3:28:04 AM10/18/16
to ISO C++ Standard - Future Proposals
Hi,

Hmm, I like it for break, but we also wanted `return;` (for return to caller's caller). Wouldn't that be confusing?

Thanks,
-lorro

szollos...@gmail.com

unread,
Jan 30, 2017, 6:10:52 PM1/30/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

[For those just joining in: it's actually 'capturing the return continuation for for-else / for-break']
I've made a few prototypes based on different approaches (incl. exceptions, continuations, wrapper macros around `for', variant return types, continuation-as-a-class, etc.). It looks to me now that these are exceptions, but inline ones. How would you like something like:

#include <vector>
#include <iostream>

extern bool g();

struct Break {};

int sum(const std::vector<int>& v)
{
   
int ret = 0;
   
for (auto&& elem : v) {
       
if (g(elem)) throw inline Break();
        ret
+= elem;
   
}
}

int main()
{
    std
::vector<int> v{ 1, 2, 3 };
   
try {
        std
::cout << sum(v);
   
} catch inline (const Break&) {
        std
::cout << "break";
   
}
}

Where `throw inline' means that only inlineable calls appear on the return path from the throw point to `catch inline'. Thus no non-final virtual calls, no non-constexpr fnptr / mem fnptr calls, definitions in the same compilation unit, etc. This means that the compiler 'sees' the return path and can compile as efficient code as an inline call would be for the various returns (possibly more efficient than a variant return type; surely more efficient than regular exceptions / continuations; also, it expresses intent more clearly). It also means that a non-constexpr function pointer cannot be formed to a function which 'leaks' inline exceptions; also, such a function can only be called from a virtual function if the virtual fn catches all exceptions.

Since these are inlineable (not necessarily inlined), one might even write template catch:
try { /* ... */
}
template<typename T>
catch (const T& t) { /* ...*/
}

Note that this does not give us language-level visitations as the try block cannot `throw inline' from a virtual call. If - and only if - one aims to solve visitation, one might consider:
enum MyEnum { a, b, c };

void visit(const MyEnum e) {
   
switch (e) {
   
template<MyEnum enumVal>
   
catch enumVal:
       
throw inline std::integral_constant<MyEnum, enumVal>();
   
}
}

The latter part is strictly optional (and can be implemented in terms of the enum reflection proposal).

Do you think it's feasible?

Thanks in advance,
-lorro
Reply all
Reply to author
Forward
0 new messages