meguli
meguli

Reputation: 1526

Implementing and wrapping function composition in C++ for lazy evaluation

Suppose I have a naive implementation of an applicative, which is a name I picked for sanity and not that I know anything about Applicative type class from other languages. Here goes the implementation:

#include <iostream>
#include <string>

template <typename T>
struct applicative {
    template <typename Fn>
    auto then(Fn f) const {
        return applicative<decltype(f(data_))>{f(data_)};
    } 

    template <typename Fn>
    auto and_last(Fn f) const {
        return f(data_);
    }
    T data_;
};

int main() {
    applicative<std::string>{"hello world"}
    .then([](std::string const& s) {return s.size() * 4; })
    .then([](int k) {return k - 2; })
    .and_last([](int k) { std::cout << k << "\n"; });
} 

Now, this could be improved in many ways. Providing something like make_applicative for in-place construction, trying to eliminate redundant copies and moves if there are any etc. But from the time I saw our ability to compose functions in C++, I feel like something better is possible. Any compose implementation would work, so let's pick the one in this codereview.stackexchange question. With it, we can say things like

auto f1 = [](std::pair<double,double> p) {return p.first + p.second; };
auto f2 = [](double x) {return std::make_pair(x, x + 1.0); };
auto f3 = [](double x, double y) {return x*y; };
auto g = compose(f1, f2, f3);

std::cout << g(2.0, 3.0) << std::endl;   //prints '13', evaluated as (2*3) + ((2*3)+1)

Pretty nice. Going back to my idea, I think this should make possible a rework of my applicative implementation in following way:

auto sf = applicative<std::string>{}
    .then([](std::string const& s) {return s.size() * 4; })
    .then([](int k) {return k - 2; });

    std::cout << sf.eval_with("hello world"); << "\n"; 

As you can see, this is sort-of lazy evaluated and we only supply value when we need it, with eval_with. I've been thinking about how to implement this new version for one hour now and I have no idea where to store operations and composed functions, what to make of applicative template parameter like we have here with std::string and many more problems. How would one implement something like this? Is it trivial as I initially hoped to be or does it require a lot of code? I really want this because I feel like this would buy me a lot by preventing a lot of argument passing on long chain of functions.

Edit: I am working on it a little more and turns out compose implementation I linked is not what I actually had in mind since we are still performing all the function calls in the chain and still passing arguments around. But you can answer assuming any compose function would work and choice of a better compose implementation would be a performance optimization. What I had in mind was more like following, taken from my example

applicative<std::string>{"hello world"}
    .then([](std::string const& s) {return s.size() * 4; })
    .then([](int k) {return k - 2; })
    .and_last([](int k) { std::cout << k << "\n"; });

Those then calls would result in a single function call equivalent to (s.size() * 4) - 2 which could be evaluated with eval_with.

Upvotes: 4

Views: 289

Answers (1)

Dmitry Gordon
Dmitry Gordon

Reputation: 2324

Is that what you want?

#include <iostream>
#include <string>

struct id
{
    template <typename T>
    auto operator()(T t) const
    {
        return t;
    }
};

template <typename T, typename Func = id>
struct applicative {
    applicative(Func f = Func())
        : _f(f)
    {
    }

    template <typename Fn>
    auto then(Fn f) const {
        auto composition = [=](T val) { return f(_f(val)); };
        return applicative<T, decltype(composition)>(composition);
    } 

    auto eval_with(T t)
    {
        return _f(t);
    }

    Func _f;
};

int main() {
    auto sf = applicative<std::string>{}
    .then([](std::string const& s) {return s.size() * 4; })
    .then([](int k) {return k - 2; });

    std::cout << sf.eval_with("hello world") << "\n"; 
} 

Disclaimer: I didn't bother about perfect forwarding, so everything is passed by value.

Upvotes: 2

Related Questions