-
Notifications
You must be signed in to change notification settings - Fork 37
Description
We should provide a mechanism for user-defined sequences (and library-defined sequences) to use custom implementations of Flux algorithms and adaptors, when they can do so more efficiently or offer more functionality than the default.
Motivation
The repeat(val) sequence factory endlessly repeats val, while repeat(val, n) repeats val exactly n times. Under the new model, the infinite version is iterable only, while the finite version is a random_access_collection. This means that repeat(val).take(n) will also be iterable-only, when it really should be exactly the same as repeat(val, n) (i.e. random-access). In order to achieve this, we need to do one of two things: either modify take so that it special-cases repeat, or provide a way for repeat to specialise the take algorithm.
If it was just repeat(val).take(n) that was the problem, then special-casing would probably be the way to go. But when you look into it, more appear where generic customisation would be useful. For example:
- Sticking with infinite
repeat: thedrop,strideandreverseadaptors could all be no-ops - Similar to
repeat(),iota(n)is iterable-only butiota(n).take(m)could be random-access take()anddrop()foriota(m, n)could be specialised to just adjust the bounds of the iota sequencetake(n).take(m)anddrop(n).drop(m)could be specialised to just adjust the counttake()anddrop()for random-access slices could be specialised to just adjust their bounds- Almost anything you do with an
emptysequence results in another empty sequence
Ranges has lots of special cases in its views function objects (in particular views::take and views::drop): it would be nice if we could do the same things but in a more principled way that end-users can also hook in to.
Mechanism
The special cases we currently have (e.g. reverse().reverse()) use the same approach as ranges, i.e. just an if constexpr inside the function object. A more generic approach would be to provide a flux::implementation class template which users could specialise:
template <typename Fn, typename... Types>
struct implementation; // not defined
template <typename Fn, typename... Types>
concept has_custom_impl = requires {
{ sizeof(implementation<Fn, Types...>) > 0 };
};We could then define take as:
struct take_fn {
template <adaptable_iterable Iter>
auto operator()(Iter&& iter, int_t count) const
{
FLUX_ASSERT(count >= 0);
using R = std::remove_cvref_t<Iter>;
if constexpr (has_custom_impl<take_fn, R>) {
return implementation<take_fn, R>{}(FLUX_FWD(iter), count);
} else {
return take_adaptor<R>(FLUX_FWD(iter), count);
}
}
};
inline constexpr auto take = take_fn{};Later, a type wanting to specialise take could do it like so:
template <typename Value>
struct implementation<take_fn, repeat_iterable<Value>> {
auto operator()(repeat_iterable<Value>&& iter, int_t count) const
{
return repeat_collection<Value>(std::move(iter).get_value(), count);
}
};The only potential down-side that I can see with this approach is that the compiler will need to (try to) instantiate implementation<F, Arg> for every function call. I'm not sure whether this would actually be measurable given the amount of concept checking etc we do anyway, but it's worth bearing in mind. Perhaps a good first step would be to do this only for the most important functions (i.e. take() and drop()) and see what the impact is.