0

I discovered about continuations while reading The Little Schemer by Friedman and Felleisen, and to practice a little with this concept I wrote a simple code that given a list of integers removes the evens and returns the sum of the odds.

This is my scheme code:

(define nil '())

; s is a list of integers, op is an operator.
(define (rem_add s op)
  (cond ((null? s) (op nil 0))
        ((even? (car s))
            (rem_add (cdr s)
                (lambda (l n) (op l n))))
        (else
            (rem_add (cdr s)
                (lambda (l n) (op (cons (car s) l) (+ n 1)))))))

(define (count s n)
  (cond ((null? s) 0)
        (else (+ (car s) (count (cdr s) n)))))

(define s (list 1 2 3 4 5))
(display (rem_add s count))

I compiled it with chicken scheme and works as expected, producing the output 9.

Then I tried to rewrite the same code in C++, as follows.

#include<functional>
#include<iostream>
#include<set>


int rem_add(const std::set<int>& s, auto op) {
    if (s.empty()) {
        return op(s, 0);
    }
    std::set<int> s2(++s.cbegin(), s.cend());
    if (*s.cbegin() % 2 == 0) {
        return rem_add(s2,
            [op](const std::set<int>& l, int n){
                    return op(l, n);
                });
    } else {
        return rem_add(s2,
            [&s, op](const std::set<int>& l, int n){
                   std::set<int> q(l);
                   q.insert(*s.cbegin());
                   return op(q, n + 1);
            });
    }
}


// Remove the even elements from s, and return the sum of the odd elements.
int main() {
    std::set<int> s {1, 2, 3, 4, 5};

    std::function<int(const std::set<int>&, int)>
      count = [count](const std::set<int>& s, int n)
        {   if (s.empty()) { return 0; }
            std::set<int> s2(++s.cbegin(), s.cend());
            return *s.cbegin() + count(s2, n);
        };
    auto sum_odds = rem_add(s, count);
    std::cout << sum_odds << std::endl;
}

Trying to compile this code with g++ 9.2.1 takes very long. After a few minutes it used up all the memory of my laptop and I had to abort the compilation. The only compiler warning I get is related, I think, just to the use of a very recent keyword:

warning: use of ‘auto’ in parameter declaration only available with ‘-fconcepts’
    7 | int rem_add(const std::set<int>& s, auto op) {
      |                                     ^~~~

I would like to know what is happening under the hood: why does the compiler take so much time and memory for what looks like a very simple piece of code?

J. D.
  • 279
  • 1
  • 9
  • 2
    Probably recursive template expansion (use of `auto` in a parameter declaration is like creating a template). See the errors from clang - live: https://godbolt.org/z/8wzgP9 – Richard Critten Mar 07 '20 at 14:32
  • @RichardCritten Indeed, if I replace `auto` in the parameter list with `std::function&, int)>`, then the compilation completes almost instantly. But then running the executable produces a segmentation fault. Probably there is a bug somewhere. – J. D. Mar 07 '20 at 14:37
  • Beyond my abilities I'm afraid. Perhaps add [language-lawyer] tag? – Richard Critten Mar 07 '20 at 14:40
  • @RichardCritten I still think that allowing the use of normal function syntax to define function templates, implicitly, via `auto`, was a serious mistake. – curiousguy Mar 08 '20 at 04:06

2 Answers2

1

Yes, in C++ it is possible to write continuations. See for example this webpage for examples.

The below code uses continuation passing style (CPS) for the example you proposed:

#include<functional>
#include<iostream>
#include<vector>

int main() {

    auto calc = [](auto&& k1, auto&& k2, auto V){ return k1(k2(V,0));};

    std::function<std::vector<int>(std::vector<int>, int)> get_odd;
    get_odd = [&get_odd](auto V, int pos)
    {
        if(pos == V.size()) return V;
        if(V[pos] % 2 == 0)
        {
            V.erase(V.begin()+pos);
            return get_odd(V,pos);
        }
        else
            return get_odd(V,++pos);
    };

    std::function<int(const std::vector<int>&)> sum;
    sum = [&sum](const std::vector<int>& V)
    {
        if (V.empty()) return 0;
        std::vector<int> W(++V.begin(), V.end());
        return V[0] + sum(W);
    };

    std::vector<int> v1{1,2,3,4,5};   
    std::cout << calc(sum, get_odd, v1) << std::endl;

    return 0;
}

The output reads

9

You can run the code online.

BlueTune
  • 1,023
  • 6
  • 18
  • I like very much your code, but how would you do it with both `calc` and `sum` as recursive functions? – J. D. Mar 07 '20 at 21:04
  • Actually, changing slightly your code to make `calc` recursive as shown [here](https://godbolt.org/z/TfLe_6) returns a segmentation fault error as in the original question. – J. D. Mar 07 '20 at 21:30
  • I modified my code so that the two relevant functions are recursive. The code runs as you can see [here](https://godbolt.org/z/7kHuQC). Should I modify my original answer with the code presented in this comment? – BlueTune Mar 08 '20 at 08:38
  • Yes, please. I think keeping the recursive lambdas as in the original question would be more useful for future readers. – J. D. Mar 08 '20 at 11:38
0

Enjoy the power of C++. Some macro magic and it is almost same as original version.

#include <functional>
#include <iostream>
#include <vector>

using Set = std::vector<int>;
using Op = std::function<int(const Set &, int n)>;
int car(const Set &s) { return *s.cbegin(); }
Set cdr(const Set &s) { return Set(++s.cbegin(), s.cend()); }
bool even(int x) { return (x % 2) == 0; }
bool null(const Set &s) { return s.empty(); }
Set cons(int x, const Set &s) { Set r = s; r.push_back(x); return r; }

#define DEFINE_OP(name, s, op) int name(const Set &s, Op op) {
#define DEFINE_N(name, s, n) int name(const Set &s, int n) {
#define DEFINE_S(name, ...) auto name = Set{__VA_ARGS__};
#define COND_START if (false) {}
#define COND(expr, result) else if (expr) { return result; }
#define ELSE(result) else { return result; } }
#define LAMBDA(l, n, body) [s, op](const Set &l, int n) { return body; }
#define DISPLAY(func, s, count) int main() { std::cout << func(s, count) << std::endl; return 0; }

// here is the implementation

DEFINE_S(nil)

DEFINE_OP (rem_add, s, op)
  COND_START
    COND (( null ( s ) ), ( op (nil, 0) ))
    COND (( even ( car(s) )),
            ( rem_add (cdr (s),
                 LAMBDA (l, n, ( op (l, n) ) ))))
    ELSE (
            ( rem_add (cdr (s),
                 LAMBDA (l, n, ( op ( cons ( car (s), l), n + 1) )))))

DEFINE_N (count, s, n)
  COND_START
    COND ( (null (s)), (0))
    ELSE ( (car (s) + count (cdr (s), n - 1 ) ))

DEFINE_S(s, 1, 2, 3, 4, 5);
DISPLAY(rem_add, s, count)
Tarek Dakhran
  • 2,021
  • 11
  • 21
  • Many people would say using macros is not idiomatic C++ (e.g. Scott Myers's *Effective C++*), but anyway: is your code resolving the recursions via compile-time substitution rules by the preprocessor? If so, this would not answer the question. And what is the purpose of COND_START? Also, why are you decrementing n in the definition of `count`? – J. D. Mar 07 '20 at 21:17
  • This code does exactly the same what your scheme snippet does. The preprocessor is used to hide ugly c++ syntax for lambdas and if-elseif-else statements. If you replace preprocessor directives with the code, you will see what happens. The answer to question is yes. C++ support continuations. And aforementioned snippet shows how. – Tarek Dakhran Mar 07 '20 at 21:51
  • Back to the `n`, it is unused. I don't remember why I decremented it. Probably thought that it denotes the size of the container. – Tarek Dakhran Mar 07 '20 at 21:54
  • Ok, thanks for the comments. I checked [here](https://godbolt.org/z/y9kBtU) that indeed the recursion works fine without preprocessor substitutions. In your example however, you define `count` as a regular function. It seems that defining `count` as a recursive lambda function does not work, and it would be interesting to know why. But I agree that both your example and BlueTune's one do show an example of continuations in C++ (in the sense of passing a lambda to a higher-order function that then modifies the lambda and calls itself to keep track of the computation and of the environment). – J. D. Mar 07 '20 at 22:24
  • 1
    You can define count as a recursive lambda function, and it will work. The trick is that first you need to declare count as `Op count;` and then define it as `count = [&count](...)`. This way compiler knows the declaration and can capture it. – Tarek Dakhran Mar 07 '20 at 22:28
  • An [example](https://godbolt.org/z/WEVDf6) with count defined as lambda. – Tarek Dakhran Mar 07 '20 at 22:32
  • The point does not seem to be the declaration of `Op count;`, but rather the capture by reference. Also in the code of my original question, just replacing `count = [count](...)` with `count = [&count](...)` solves the problem. I do not know why, but this is some progress. – J. D. Mar 07 '20 at 22:39
  • 1
    Not everyone has the luxury to use standard higher that `c++11`. Without forward declaration, it will not work before `c++14`. – Tarek Dakhran Mar 07 '20 at 22:42
  • I updated the question, in case you want to explain further why we had to capture the lambda by reference. – J. D. Mar 07 '20 at 22:56
  • Now the question is duplicate of https://stackoverflow.com/questions/57561407/why-is-this-recursive-lambda-function-unsafe , flagged to close. – Tarek Dakhran Mar 07 '20 at 23:12