In the Fit library it has the fit::unpack function adaptor that will unpack a sequence onto a function. It works with both std::tuple and fit::pack :

auto check_equal = [](auto x, auto y) { return x == y; } assert(fit::unpack(check_equal)(std::make_tuple(1, 1))); assert(fit::unpack(check_equal)(fit::pack(1, 1)));

It is also extensible, so by specializing fit::unpack_sequence it can unpack other sequences as well. This post won’t focus on the extensibility of fit::unpack , but rather what can be done with fit::unpack when combined with other components in Fit.

Basic algorithms

We can implement simple algorithms such as transform (also know as a functor) by using the fit::by adaptor. The adaptor applies a projection over each of the parameters of the function. For the transform algorithm, the function is just the construction of std::tuple with the projection being applied to each parameter(or element) of the tuple:

FIT_STATIC_LAMBDA_FUNCTION(transform) = [](auto&& sequence, auto f) { return fit::unpack(fit::by(f, fit::construct<std::tuple>()))(std::forward<decltype(sequence)>(sequence)); };

Now, if we leave off the fit::construct<std::tuple>() then fit::by will still call the projection on each parameter, it just won’t pass the result to another function. This is known as for_each :

FIT_STATIC_LAMBDA_FUNCTION(for_each) = [](auto&& sequence, auto f) { return fit::unpack(fit::by(f))(std::forward<decltype(sequence)>(sequence)); };

So algorithms can be written by combining adaptors with fit::unpack . Another example is fold which can be implemented using fit::compress :

FIT_STATIC_LAMBDA_FUNCTION(fold) = [](auto&& sequence, auto f) { return fit::unpack(fit::compress(f))(std::forward<decltype(sequence)>(sequence)); };

Filter

Well, filter needs a little more work as the Fit library doesn’t provide a filter_adaptor . We can implement it by using a combination of join and transform (also know as a monad). First, we transform it to a sequence holding one element if it matches the predicate, otherwise, it will return an empty sequence. Next, we do a join which will flatten all these sequences together into one sequence. The empty sequences fall off, leaving just the elements that match the predicate.

So first, lets look how to implement join . This is actually very easy, since fit::unpack can unpack multiple sequences. All the elements from each sequence will be called on the function in order:

auto check_equal = [](auto x, auto y) { return x == y; } assert(fit::unpack(check_equal)(std::make_tuple(1), std::make_tuple(1))));

So fit::unpack(fit::construct<std::tuple>()) is equivalent to tuple_cat , however, for join it takes a sequence of sequences, so its just a matter of doing a double unpack:

FIT_STATIC_FUNCTION(join) = fit::unpack(fit::unpack(fit::construct<std::tuple>()));

So now we compose this with transform to implement filter . Now we can’t check the predicate using a runtime if , we actually need a static_if . C++ doesn’t provide one, but we can use fit::if_ with fit::conditional to check the predicate:

FIT_STATIC_LAMBDA_FUNCTION(filter) = [](auto&& sequence, auto predicate) { return fit::compose(join, transform)( std::forward<decltype(sequence)>(sequence), [&](auto&& x) { return fit::conditional( fit::if_(predicate(std::forward<decltype(x)>(x)))(fit::pack), fit::always(fit::pack()) )(std::forward<decltype(x)>(x)); } ); };

Of course, all predicates must be dependently-typed, which is very natural to use:

auto t = std::make_tuple(1, 2, 'x', 3); auto r = filter(t, [](auto x) { return std::is_same<int, decltype(x)>(); }); assert(r == std::make_tuple(1, 2, 3));

Zipping sequences

The zip function combines two sequences so they can be unpacked together. Now, the Fit library does provides fit::combine that combines each function with a corresponding parameter. It is essentially zipping them together. However, we first need to transform the first sequence to a sequence of functions:

FIT_STATIC_LAMBDA_FUNCTION(zip_with) = [](auto&& sequence1, auto&& sequence2, auto f) { auto&& functions = transform( std::forward<decltype(sequence1)>(sequence1), [&](auto&& x) { return [&](auto&& y) { return f(std::forward<decltype(x)>(x), std::forward<decltype(y)>(y)); }; } ); ... };

Next, we can unpack that sequence with fit::combine , however, the first function to combine will be the construction of std::tuple :

FIT_STATIC_LAMBDA_FUNCTION(zip_with) = [](auto&& sequence1, auto&& sequence2, auto f) { auto&& functions = transform( std::forward<decltype(sequence1)>(sequence1), [&](auto&& x) { return [&](auto&& y) { return f(std::forward<decltype(x)>(x), std::forward<decltype(y)>(y)); }; } ); auto combined = fit::unpack(fit::capture(fit::construct<std::tuple>())(fit::combine))(functions); ... };

Next we unpack combined with the second sequence:

FIT_STATIC_LAMBDA_FUNCTION(zip_with) = [](auto&& sequence1, auto&& sequence2, auto f) { auto&& functions = transform( std::forward<decltype(sequence1)>(sequence1), [&](auto&& x) { return [&](auto&& y) { return f(std::forward<decltype(x)>(x), std::forward<decltype(y)>(y)); }; } ); auto combined = fit::unpack(fit::capture(fit::construct<std::tuple>())(fit::combine))(functions); return fit::unpack(combined)(std::forward<decltype(sequence2)>(sequence2)); };

So now we can use it to, for example, compute the dot product of two tuples:

FIT_STATIC_LAMBDA_FUNCTION(dot) = [](auto&& x, auto&& y) { auto product = zip_with(x, y, [](auto x, auto y) { return x*y; }); return fold(product, [](auto x, auto y) { return x+y; }); };

Conclusion

We can actually accomplish a lot using fit::unpack . We just wrote a simple extensible heterogeneous algorithms library in under 100 lines of code, and without using template template parameters.

Of course, this doesn’t handle laziness, but for most cases, this is not necessary. If it is necessary, than some form of thunks could be used for computation instead.