Could std::some-namespace::transform one day support any functor?
Asked Answered
I

2

8

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 the different order of the two arguments, but this is not a big deal).

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. No quite, breaking code that relies on SFINAE-based detection of invalid code is ok.


(¹) 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

Igloo answered 9/1, 2022 at 9:58 Comment(3)
Is there some technical reason that makes you think adopting these changes would be impossible/hard?Helpful
@idmean, I've mentioned one possible problem. SFINAE code relying on the non validity of some code would break.Igloo
I can only speculate, but I find it very unlikely. It was painful enough getting those optional operations in. I don't personally see the committee embracing category theory for the future even if there are certainly people on it who are familiar with Haskell or similar.Corm
P
3

And this makes me think that there could be some generic interface, name it mbind, that one can opt in with any type. ...

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

There is P0650 which proposes a non-member monadic interface, customizable using traits. The paper shows customizations for expected and other types, implementable in C++17.

The accepted proposal for std::optional monadic operations P0798 §13.1 references P0650 while discussing alternatives to member-function syntax:

Unfortunately doing the kind of composition described above would be very verbose with the current proposal without some kind of Haskell-style do notation

std::optional<int> get_cute_cat(const image& img) {    
   return functor::map(
       functor::map(
         monad::bind(
           monad::bind(crop_to_cat(img),
             add_bow_tie),
           make_eyes_sparkle),
        make_smaller),
     add_rainbow);
 }

My proposal is not necessarily an alternative to [P0650]; compatibility between the two could be ensured and the generic proposal could use my proposal as part of its implementation.

It goes on to mention how other C++ features still in development, like unified call syntax, might provide a more concise syntax for generic monadic operations.

The accepted proposal for std::expected monadic operations P2505 doesn't reference P0650 directly, but discusses "Free vs member functions" as part of its design considerations, ultimately prioritizing consistency with the std::optional monadic interface.

Putty answered 16/2, 2023 at 0:29 Comment(0)
E
2

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), although it is just views::transform | views::join under the hood.

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 Edit: I realised how to handle applicatives.

Applicative<int> a, b, c;
auto out = a
    | zip_transform(b, c
        , [](int a, int b, int c){ return a + b + c; })
    ;

zip_transform because it zips a, b, c into a Applicative<std::tuple<int, int, int>> and then transforms it (think optional, future, range). Although you could always work with Applicatives of partially applied functions, but that would involve lots of nested functions which is not the style of c++ and would disrupt the top to bottom reading order.

Emmalynn answered 28/6, 2022 at 23:6 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.