The code of this article builds on the code of the prior.
Zipping.
If we have several tuples, what if we want to apply a function to the nth element of each one?template< template<size_t> class Fi = Get, size_t i, class F, class ...T > constexpr auto zipRow( const F& f, T&& ...t ) -> decltype( f(Fi<i>()(std::forward<T>(t))...) ) { return f( Fi<i>()( std::forward<T>(t) )... ); }
Not hard at all! It basically squishes that row (thinking of t as a column and ti... as a row), using f, into one value. Now, let's say we want to zip together t... into one tuple.
template< template<size_t> class Fi = Get, size_t ...i, class Ret, class F, class ...T > constexpr auto zipIndexList( IndexList<i...>, const Ret& r, const F& f, T&& ...t ) -> decltype( r(zipRow<Fi,i>(f,std::forward<T>(t)...)...) ) { return r( zipRow< Fi, i >( f, std::forward<T>(t)... )... ); } template< template<size_t> class Fi = Get, class Ret, class F, class T, class ...U, class _T = typename std::decay<T>::type, class IL = typename IListFrom<_T>::type > constexpr auto zipTupleTo( const Ret& r, const F& f, T&& t, U&& ...u ) -> decltype( zipIndexList<Fi>(IL(),r,f,std::forward<T>(t),std::forward<U>(u)...) ) { return zipIndexList<Fi>( IL(), r, f, std::forward<T>(t), std::forward<U>(u)... ); } int main() { auto zipped = zipTupleTo( tuple, std::plus<int>(), tuple(1,10), tuple(2,20) ); std::cout << " 1 + 2 = " << std::get<0>(zipped) << std::endl; std::cout << "10 + 20 = " << std::get<1>(zipped) << std::endl; }
In zipIndexList, r represents the function defining how the output is returned. tuple (gist), from the previous article, is just a function object form of std::make_tuple that can be passed to higher order functions. By supplying it as our r, we're saying "just make it a tuple again."
Since most often, we want to zip back into a tuple, it makes sense to define zipTuple like so:
template< template<size_t> class Fi = Get, class F, class ...T > constexpr auto zipTuple( const F& f, T&& ...t ) -> decltype( zipTupleTo<Fi>(tuple,f,std::forward<T>(t)...) ) { return zipTupleTo<Fi>( tuple, f, std::forward<T>(t)... ); }
zipTuple is to tuples what std::transform is to sequences. The drawback of std::transform is that it only allows for a unary transformation or binary. Let's write a version that accepts any number of arguments.
// We require these polymorphic function objects. constexpr struct Inc { template< class X > constexpr X operator () ( X x ) { return ++x; } } inc{}; constexpr struct Eq { template< class X > constexpr bool operator () ( const X& a, const X& b ) { return a == b; } } eq{}; struct Or { template< class X > constexpr bool operator () ( const X& a, const X& b ) { return a || b; } }; // Wrapper to dereference arguments before applying. // indirect : (a -> b) -> (a* -> b) template< class F > struct Indirect { F f = F(); constexpr Indirect( F f ) : f(std::move(f)) { } template< class ...It > constexpr auto operator () ( It ...it ) -> decltype( f(*it...) ) { return f( *it... ); } }; template< class F, class I = Indirect<F> > constexpr I indirect( F f ) { return I( std::move(f) ); } #include <vector> #include <algorithm> template< class F, class ...X, class Result = typename std::result_of<F(X...)>::type, class Ret = std::vector<Result> > Ret transform( const F& f, const std::vector<X>& ...vs ) { Ret r; const auto ends = tuple( vs.end()... ); // Iterate through each vector in parallel. for( auto its = tuple( vs.begin()... ); // This unrolls to: not (it0==end0 || it1==end1 || ...) not foldl( Or(), zipTuple(eq,its,ends) ); // Increment each iterator. its = zipTuple( inc, its ) ) { r.emplace_back ( applyTuple( indirect(f), its ) ); } return r; } int main() { std::vector<int> v = {1,10,100}, w = {2,20,200}, x = {3,30,300}; auto vw = transform ( [](int x, int y, int z){ return x+y+z; }, v, w, x ); std::cout << " 1 + 2 + 3 = " << vw[0] << std::endl; std::cout << " 10 + 20 + 30 = " << vw[1] << std::endl; std::cout << "100 + 200 + 300 = " << vw[2] << std::endl; }
Note: foldl (gist).
Mapping.
Suppose we want to know the results of adding every combination of {1,2,3} with {9,8,7}. We could write a function that cross-applied every variable from each tuple, but slightly more generally, we can start by taking the Cartesian product.// product : {x,y} x {a,b} -> {{x,a},{x,b},{y,a},{y,b}} constexpr struct Product { // {...xi...} x {...aj...} -> {xi,aj} template< size_t i, size_t j, class T, class U > static constexpr auto zip( const T& t, const U& u ) -> decltype( tuple(std::get<i>(t),std::get<j>(u)) ) { return tuple( std::get<i>(t), std::get<j>(u) ); } // {...xi...} x {a0,a1,a2...} -> { {xi,a0}, {xi,a1}, ... } template< size_t i, size_t ...j, class T, class U > static constexpr auto withI( IndexList<j...>, const T& t, const U& u ) -> decltype( tuple(zip<i,j>(t,u)...) ) { return tuple( zip<i,j>(t,u)... ); } // {x...} x {a...} -> { {x,a}... } template< size_t ...i, size_t ...j, class T, class U > static constexpr auto withIndexes( IndexList<i...>, IndexList<j...> js, const T& t, const U& u ) -> decltype( std::tuple_cat(withI<i>(js,t,u)...) ) { return std::tuple_cat( withI<i>(js,t,u)... ); } template< class T, class U, class IL = typename IListFrom<T>::type, class IL2 = typename IListFrom<U>::type > constexpr auto operator () ( const T& t, const U& u ) -> decltype( withIndexes(IL(),IL2(),t,u) ) { return withIndexes( IL(), IL2(), t, u ); } } product{};We can now define a map operation to apply the product.
template< class F > struct ApplyF { F f = F(); constexpr ApplyF( F f ) : f(std::move(f)) { } template< class T > constexpr auto operator () ( T&& t ) -> decltype( applyTuple(f,std::forward<T>(t)) ) { return applyTuple( f, std::forward<T>(t) ); } }; template< class F > constexpr ApplyF<F> applyF( F f ) { return ApplyF<F>(std::move(f)); } constexpr struct MapTuple { template< class F, class T, class U > constexpr auto operator () ( const F& f, const T& t, const U& u ) -> decltype( zipTuple(applyF(f),product(t,u)) ) { return zipTuple( applyF(f), product(t,u) ); } } mapTuple{}; int main() { auto sums = mapTuple( std::plus<int>(), tuple(1,2,3), tuple(7,8,9) ); std::cout << "map (+) (1,2,3) (7,8,9) = "; forEach( printItem, sums ); std::cout << std::endl; }
This prints out:
map (+) (1,2,3) (7,8,9) = 8 9 10 9 10 11 10 11 12
Zipping applies elements across from each other. Mapping applies everything to everything. (Note: a unary definition of map would be equivalent to a unary definition of zip.)
Tuples as function environments.
This might seem a little off topic, but Haskell has this neat function, id. It works like this:id x = x
Simple, right?
(id f) x y = f x y = id f x y
id has this neat property that if applied multiple arguments, it applies the tail arguments to the first. This is an artifact of Haskell's curried notation, but we can emulate this behaviour:
constexpr struct Id { template< class X > constexpr X operator () ( X&& x ) { return std::forward<X>(x); } template< class F, class X, class ...Y > constexpr auto operator () ( const F& f, X&& x, Y&& ...y ) -> typename std::result_of< F(X,Y...) >::type { return f( std::forward<X>(x), std::forward<Y>(y)... ); } } id{};And now tuples take on a new role: Contained function environments. Consider:
applyTuple( id, tuple(std::plus<int>(),1,2) );
What does this output? How about
mapTuple( id, tuple(inc,dec), tuple(5,9) );
auto pm = tuple(std::plus<int>(),std::minus<int>());
zipTuple( id, pm, tuple(10,5), tuple(10,5) );
Or:
mapTuple( id, pm, tuple(1,2), tuple(3,4);
I leave implementing the three-tuple version of mapTuple as an exercise, but here's a hint: cross( cross({f},{x}), {y}) = {{{f,x},{y}}}, but you need to take it from that to {{f,x,y}}. (Another good exercise might be to write zipTuple in terms of transposition (wiki).)
Conclusions.
This flushes out some basic applications of tuples to functions. applyTuple unpacks a tuple and applies it to a function. foldl and foldr let one apply binary functions to nary tuples, or even singletons (maths concept, not design pattern). zipTuple transforms multiples tuples by a functions, member-wise. mapTuple performs a function for every combination of arguments.Tuples have unusual mathematical properties compared to other data structures due to the profundity of what they generalize. They can help us shorthand functions to operate in parallel (zip), be passed around as partial or complete function environments, control variadic template parameters, and much, much more that I have either not discussed or yet realized.
One use I haven't discussed, for example, is as a relation, but for an example of this use of tuples, look no further than std::map.
I hope this post has been interesting. Happy coding!
Source for this article: https://gist.github.com/4268029
Interesting article! However, I'm still missing a compelling reason to use such techniques. It is a bit exhausting to understand the code. What's the big picture here?
ReplyDeleteYou might be interested in a blog post I wrote on "Generic Transpose using boost tuples". It is really a generic zip operation on vectors not a transpose. I'm using that facility in a generic XML processing library.
I have to admit, I'm not sure where the big picture is, really, because I only recently started playing around with them (largely thanks to articles such as yours). The above, and the previous article, are a mathematical basis from which more complex algorithms can be built. But math is only useful when applied.
DeleteWith the example of the article you mention, instead of writing a complex object that handles all the logic at once, you can write simple, small, obvious functions and describe them in terms of folds, maps, and zips. Using "translate" from this article, you could achieve the same affect with
translate( tuple, v1, v2, v3, ... );
where "tuple" (not std::tuple) is defined as a polymorphic function object [1].
Also, with this technique, you do not have to write a class to deduce the type of the result, you can use std::result_of.
[1]: https://gist.github.com/4268029#file-zip-map-tuple-cpp-L97
In my view, 'the big picture' is in the elegance with how certain type of problems can be solved by using C++11 core constructs (tuple, variadic templates, meta functions etc.). Myself, I'm using 'tuple based programming' quite a lot for database access where bind variables, rows and other constructs are mapped to columns in varous ways. Here tuple based programming solves many problems in neat and clean ways. Specifically, I use (and wrote) a thin wrapper around Oracle OCCI (could be done for any database API) which aligns the API with 'STL style programming' making database access in C++ both elegant, simple and as type safe as is possible in just with a few hundred lines of code in a few headers.
Delete