joboccara/pipes

element-wise transform on mux'd pipes

tnovotny opened this issue · 5 comments

It would be nice if element-wise transformations like this worked on mux'd streams.

std::vector<int> a{ 1, 2, 3, 4 };
std::vector<int> b{ 11, 22, 33, 44 };

mux( a, b ) >>=transform( []( auto i, auto j ) {
      return std::make_tuple( i * 10, j - 5 );
    } )
    >>=  filter( []( auto i, auto j ) { return i + j > 30.0; } )
    >>= to_out_stream( std::cout );

Since this does something different than passing on the returned value, it might be desirable to make a new/different pipe and to keep the existing behavior under the existing name.

@tnovotny
Hey Thomas,
it actually works - but your code requires a few corrections:

  1. the filter would expect one parameter only: a std::tuple<int,int> - auto expanding to int i, int j I think would not work in C++
  2. also the to_out_stream would expect a tuple - and the ostream << operation is probably not defined. Or did you expect it to accept two parameters? And what would the output then be?

So - this would work - define the operator

std::ostream& operator << (std::ostream& os, std::tuple<int, int> const& t) {
    return os << "(" << std::get<0>(t) << "," << std::get<1>(t) << ")";
}

and the following code should work then

TEST_CASE("streamed filtered mux")
{
    std::vector<int> a{ 1, 2, 3, 4 };
    std::vector<int> b{ 11, 22, 33, 44 };
    auto resultStream = std::ostringstream{};
    std::string expected = "(20,17)(30,28)(40,39)";

    pipes::mux(a, b) 
        >>= pipes::transform([](auto i, auto j) { return std::make_tuple(i * 10, j - 5); })
        >>= pipes::filter([](auto const& t) -> bool { return std::get<0>(t) + std::get<1>(t) > 30.0; })
        >>= pipes::to_out_stream(resultStream);

    auto const results = resultStream.str();

    REQUIRE(results == expected);
}

Yes, that works, but not the way that I would like it to work. My code example did not compile, but the change I wanted was to make it compile without changing my code. See non exhaustive pipes

I added extra code in my transform to handle the case where the transformation returns a tuple,

@tnovotny
Yeah well - maybe not everybody cross-reads about all your issues...
OK - back to my question: what would the expected output of your call be - without changing your code.
Can you define that?

OK, you are right, maybe that answer didn't clear things up as much as I had hoped, and after some rethinking, there is the issue of what to_out_stream would do with the values. To get the output of your test case the user would need a way to pass a functor with formatting instructions, so having to_output_stream at the end would not work. In the cross linked issue there is a link to this example which works around that like this:

template<class TupType, size_t... I>
void
print_values( TupType const& _tup, std::index_sequence<I...> ) {
  ( ..., ( std::cout << ( I == 0 ? "" : ", " ) << std::get<I>( _tup ) ) );
  std::cout << "\n";
}

struct print : pipe_base {

  template<typename... Values>
  void
  recv( Values&&... values ) {
    print_values( std::make_tuple( values... ), std::make_index_sequence<sizeof...(values)>());
  }

  bool
  done() const {
    return false;
  }
};

int
main() {
  std::vector<int> a{ 1, 2, 3, 4 };
  std::vector<int> b{ 11, 22, 33, 44 };
  
  mux( a, b ) >>= filter{ []( auto i, auto j ) { return i + j > 20.0; } }
      >>= transform{ []( auto i, auto j ) {
        return std::make_tuple( i * 10, j - 5 );
      } }
      >>= take{ 1 }
      >>= print{};
}

and produces the output

20, 17

@tnovotny
OK - I think I finally got what you meant.
transform pushes the returned value to the next pipe. In your case the kind of inconsistency we get is that as long as the result is not transformed the muxed values are pushed to the next pipe, but once we insert a transform there is no way to get back to that form.
Using a zip-like command would maybe be less surprising (but maybe less efficient).
On the other hand I would find it rather surprising to get tuples automatically unpacked - what if I wanted to use the tuples - like push them into a vector?

The thing is - and I think that is a special case in your example - that transform here returns the same types as forwarded to it. Generally you can create anything here - convert the items to a list, a string, add them, multiply them, converting to an optional tuple ....

Maybe the pipes library could add a type (derived from tuple) that is automatically unpacked to the next pipe when returned? I think that might be more difficult to implement with C++-14 (which pipes is implemented in) and rather easy in C++-17 or later. But that might be a good solution because then the output - or rather the input for the next pipe - would be well defined.

maybe call it pipes::pack? pipes::tuple?

std::vector<int> a{ 1, 2, 3, 4 };
std::vector<int> b{ 11, 22, 33, 44 };

mux( a, b ) >>=transform( []( auto i, auto j ) {
      return pipes::pack{ i * 10, j - 5 };
    } )
    >>=  filter( []( auto i, auto j ) { return i + j > 30.0; } )
    >>= print{};

btw that would be a general solution and independent from mux