Skip to content

Customisable algorithms #253

@tcbrindle

Description

@tcbrindle

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: the drop, stride and reverse adaptors could all be no-ops
  • Similar to repeat(), iota(n) is iterable-only but iota(n).take(m) could be random-access
  • take() and drop() for iota(m, n) could be specialised to just adjust the bounds of the iota sequence
  • take(n).take(m) and drop(n).drop(m) could be specialised to just adjust the count
  • take() and drop() for random-access slices could be specialised to just adjust their bounds
  • Almost anything you do with an empty sequence 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions