9

I have a pretty simple flatmap function implemented in C++ for std::vector, but it has been suggested that ranges are generally better. Here's the vector based solution:

// flatmap: [A] -> (A->[B]) -> [B]    
template<typename T, typename FN>
static auto flatmap(const std::vector<T> &vec, FN fn) 
     -> std::vector<typename std::remove_reference<decltype(fn(T())[0])>::type> {
    std::vector<typename std::remove_reference<decltype(fn(T())[0])>::type> result;
    for(auto x : vec) {
        auto y = fn(x);
        for( auto v : y ) {
            result.push_back(v);
        }
    }
    return result;
};

It has also been suggested that I use iterators, but that breaks the nice composability of the function:

map(filter(flatmap( V, fn), fn2), fn3)

I would assume that in a range-v3 world I'd be aiming for writing the above as:

auto result = v | flatmap(fn) | filter(fn2) | transform(fn3);

It feels like flatmap should just be a trivial combination of views::for_each, yield_from and transform, but I'm struggling to work out how to hook them all together.

user2807083
  • 2,962
  • 4
  • 29
  • 37
Michael Anderson
  • 70,661
  • 7
  • 134
  • 187

3 Answers3

11

IIUC, your flatmap function is nothing but range-v3's view::for_each. Try:

using namespace ranges; auto result = v | view::for_each(fn) | to_vector;

HTH

Eric Niebler
  • 5,927
  • 2
  • 29
  • 43
  • 1
    `flatmap` takes two arguments, a list of type A and a function mapping each A to a list of B, it returns each A converted to a a list of Bs and then flattened into a single list of B. Doesn't this solution using `for_each` return instead a list of lists of B, not the flattened list. (ie this has type signature like `[A] x (A->[B]) -> [[B]]` rather than `[A] x (A->[B]) -> [B]`.). It probably just needs something added between the `for_each` and the `to_vector` - maybe the `action::join` from @user2807083 s anser? – Michael Anderson May 18 '18 at 00:02
  • 1
    No, `view::for_each` is the same as `view::transform` followed by a `view::join`. It flattens as its last step. – Eric Niebler Oct 07 '18 at 14:36
2

If I understood correctly, what your function flatmap must do, you can write it as v | view::transform(fn) | action::join. Here is example of making it with ranges:

#include <range/v3/all.hpp>

#include <iostream>
#include <string>
#include <utility>
#include <vector>


// flatmap: [A] -> (A->[B]) -> [B]
template<typename T, typename FN>
static auto flatmap(const std::vector<T> &vec, FN fn)
     -> std::vector<typename std::remove_reference<decltype(fn(T())[0])>::type> {
    std::vector<typename std::remove_reference<decltype(fn(T())[0])>::type> result;
    for(auto x : vec) {
        auto y = fn(x);
        for( auto v : y ) {
            result.push_back(v);
        }
    }
    return result;
};

// This will be test function for both flatmap and range usage
std::vector<std::string> testFn(int n)
{
    std::vector<std::string> result;
    int ofs = 0;
    for(int i = 0; i < n; ++i)
    {
        char initialChar = 'A' + ofs;
        std::string partialResult = "\"";
        for(int j = 0; j <=i; ++j, ++ofs)
        {
            partialResult.append(1, initialChar+j);
        }
        partialResult += "\"";
        result.push_back(partialResult);
    }
    return std::move(result);
}

int main(int, char**)
{
    std::vector<int> vv {1, 2, 3, 4, 5, 6};
    // test flatmap
    auto r2 = flatmap(vv, testFn);
    for(auto s:r2)
    {
        std::cout << s << " " ;
    }
    std::cout << "\n";

    using namespace ranges;

    // test ranges equivalent
    auto rng = vv|view::transform(testFn)|action::join;
    //         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this is an equivalent for flatmap in ranges terms

    for(auto s:rng)
    {
        std::cout << s << " ";
    }
    std::cout << "\n";

    std::cout << std::flush;
    return 0;
}
Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
user2807083
  • 2,962
  • 4
  • 29
  • 37
2

Both answers are correct, but I wanted to add a little more context since the naming of for_each can be a bit confusing (it did confuse me). Here is an example you can use to verify that view::for_each is actually range's flatMap:

#include <range/v3/all.hpp>
#include <iostream>
#include <vector>
using namespace ranges;

int main()
{
    const std::vector<int> a = { 0, 1, 2 };
    auto b = a | view::for_each([] (int x) { return view::ints(x, x+3); });

   ranges::for_each( b, [] (int x) { std::cout << x << " "; } );
   std::cout << std::endl;
}

This will print 0 1 2 1 2 3 2 3 4. The example also shows a possible source for confusion, since there is actually a function for_each in the ranges namespace whose functionality is similar to that of e.g. Java's forEach (i.e. applying a function to each member of a range without a return value).

If you look at the documentation of view::for_each you see that it is acutally implemented using transform and join.

 auto   operator() (Rng &&rng, Fun fun) const -> decltype(join(transform(static_cast< Rng &&>(rng), std::move(fun))))
Corinna
  • 165
  • 7