Could std::foo::transform one day support any functor?

Viewed 229

std::transform from the <algorithm> header applies to ranges, and it is what "enables" us to use ranges as the functors they are (in the sense of category theory(¹)). std::transform is iterator-based, yes, but std::ranges::views::transform is not, and its signature closely matches the signature of corresponding functions in functional languages, modulo different order of the two arguments.

When I saw this question (and in the process of answering to it), I learned that C++23 introduces std::optional<T>::transform, which makes std::optional a functor too.

All these news truly excite me, but I can't help thinking that functors are general, and that it'd be nice to have a uniform interface to transform any functor, as is the case in Haskell, for instance.

This makes me think that an object similar to std::ranges::views::transform (with a different name not alluding to ranges) could be made a customization point that the STL would customize not just for ranges, but also for std::optional and for any other functor in the STL, whereas the programmer could customize it for their user-defined classes.

Quite similarly, C++23 also introduces std::optional<T>::and_then, which is basically the monadic binding for std::optional. I'm not aware of any similar function that implements monadic binding for ranges, but C++20's some_range | std::ranges::views::transform(f) | std::ranges::views::join is essentially the monadic binding of some_range with f.

And this makes me think that there could be some generic interface, name it mbind, that one can opt in with any type. The STL would opt in for std::optional by implementing it in terms of std::optional<T>::and_then, for instance.

Is there any chance, or are there any plans that the language will one day support such a genericity?


I can certainly see some problems. Today std::ranges::views::transform(some_optional, some_func) is invalid, so some code might be relying on that via SFINAE. Making it suddenly work would break the code.


(¹) As regards the word functor, I refer to the definition that is given in category theory (see also this), not to the concept of "object of a class which has operator() defined"; the latter is not defined anywhere in the standard and is not even mentioned on cppreference, which instead uses the term FunctionObject to refer to

an object that can be used on the left of the function call operator

1 Answers

I'm not aware of any similar function that implements monadic binding for ranges, but C++20's some_range | std::ranges::views::transform(f) | std::ranges::views::join is essentially the monadic binding of some_range with f.

ranges::views::for_each is monadic bind for ranges (read), and I prefer it over views::transform | views::join due to (past?) complication/typing issues with range-v3.

As for whether you'll get a generic interface for Functor and Monad. I doubt it unless such genericity will be useful to library writers writing templates. std::expiremental::future is monadic also (and I imagine Executors are too), so one could to write generic algorithms such as foldM over these three types. I think Erik Niebler has shown with range-v3 that it is possible to write a Functor/Monad library at the expense of hand coding every pipe operator i.e.

#include <fp_extremist.hpp>
template <typename M> requires Monad<M>
auto func(M m)
{
    return m
        | fp::extremist::fmap([](auto a) { return ...; })
        | fp::extremist::mbind([](auto b) { return ...; })
        ;
}

What I think is actually possible, is that we'll get UFCS and a |> operator so we can get the benefits of invocable |> east syntax and the ability to pipe to algorithms. From Barry's blog:

It doesn’t work because while the range adapters are pipeable, the algorithms are not. ...

That is, x |> f still evaluates as f(x) as before… but x |> f(y) evaluates as f(x, y).

P.S. it's not hard to give the definition of a Functor in c++: <typename T> struct that provides transform. P.S.S It's also unclear how to handle applicatives. You want to write something like

auto out = std::optional{1}
    , std::optional{1}
    | fp::extremist::app([](auto a, auto b){...})
    ;

and it be converted into

auto out = [](auto a){ return [](auto b) {...}; }
    | fp::extremist::fmap(std::optional{1})
    | fp::extremist::app(std::optional{1})
    ;
Related