3

Edit 2:

New edit: it looks like C++20 has a new ranges library, which does what I want from the functional point of view. How would something similar be done on C++17 or earlier? Also, would the Kotlin syntactic sugar be possible? Mainly the person example:

val adam = Person("Adam").apply { 
    age = 20 // same as this.age = 20 or adam.age = 20
    city = "London"
}

Edit 1:

I don't know if my question was that clear, so I'll give an example using Rust's map.

This is how a map is done in rust:

let newVector = myVector.iter().map(|x|, x * 2)

This is how it is done in C++

std::string s("hello");
std::vector<std::size_t> ordinals;
std::transform(s.begin(), s.end(), std::back_inserter(ordinals),
     [](unsigned char c) -> std::size_t { return c; });

This is a lot more verbose.

I'd like to add some syntactic sugar to do something like this instead:

std::string s("hello");
auto ordinals = my_ns::map(s,[](unsigned char c) -> std::size_t { return c; });

The implementation of my_ns::map could be something like this (I'm sure this will not work, it's just to show how it could be done)

template<typename T, U>
map(T t, std::function<U> f)
{
    std::vector<U> ordinals;
    std::transform(t.begin(), t.end(), std::back_inserter(ordinals),
     [](U c) -> f(c));
    return ordinals;
}

In this case ordinals doesn't need to be of type std::vector<std::size_t>, it could be of a type map which has a conversion for std::vector<T>. The reason for a type map is to be able to chain functions like reduce with map.

Original Question

Note: this is for a personal project only, I do not intend to use it in production or when working with C++ most of the time.

I have been using a lot of Kotlin lately and I'm dabbling a little with Rust and I love their higher level features. That got me wondering, can I create some syntactic sugar to emulate some of these features?

Some of the stuff I'd be trying to emulate(just an example)

Rust map:

let newVector = myVector.iter().map(|x|, x * 2)

Kotlin let

val adam = Person("Adam").apply { 
    age = 20 // same as this.age = 20 or adam.age = 20
    city = "London"
}

val str = "Hello"
str.let {
   println("The string's length is ${it.length}")
}

I do not plan on using the exact same syntax, but I'd like to know if it would be possible to do something like:

int[] arr = {1, 2, 3};
auto d_arr = map(arr, [](int x){return x*2}) // maybe return a map type to be able to use .reduce, etc

The first kotlin let example I wouldn't know how to do, but the second could be solved using lambdas as well.

I'm thinking of using new types and create conversions to std::vector and arrays, kind of how C#'s LINQ works.

Any tips on how to do this are appreciated.

Ps: I know about std::transform and other functions(I'd probably use them when implementing this), but I'd like a simpler API than the ones offered (and I also think that it would be a nice personal project).

Samuel
  • 378
  • 1
  • 9
  • Yes, how to do something like `map(arr, [](int x){return x*2})` where I can then concatenate with calls to other functions like reduce, etc. I basically want to know a good way to add syntactic sugar to cpp, if it is possible. – Samuel Feb 08 '22 at 02:45
  • 2
    It sounds like you may be looking for C++20's [ranges](https://en.cppreference.com/w/cpp/ranges). Your example roughly translates to `auto d_arr = arr | views::transform([](int x){ return x*2; });`. [demo](https://godbolt.org/z/PnneWaYf4) – Brian61354270 Feb 08 '22 at 03:02
  • Damn, I need to study more some C++20, at a glance this looks pretty great. I have edited my post to try and make the question clearer. – Samuel Feb 08 '22 at 03:07
  • Suggest improving the title to relate more specifically to the question being asked about – M.M Feb 08 '22 at 03:22
  • "This is how it is done in C++" does not seem correct: the original example produces a transformed copy of the array (if I understand it correctly), while your C++ version transforms in-place. – M.M Feb 08 '22 at 03:24
  • As for as the Kotlin thing, it depends on how `Person` is declared. – Jerry Jeremiah Feb 08 '22 at 03:24
  • @M.M what do you think would be a better title? As for the C++ example, std::transform needs to have some container passed to it to hold the data. I don't believe that exists a version which directly returns a new container for you (C++17) – Samuel Feb 08 '22 at 03:28
  • @JerryJeremiah in this case Person("Adam") creates a new object from the class person. Age and city are both variables within the class (they are not an argument to the constructor). – Samuel Feb 08 '22 at 03:31
  • usually you can use boost for most modern C++ features: [Is there a range class in C++11 for use with range based for loops?](https://stackoverflow.com/q/7185437/995714) – phuclv Feb 08 '22 at 04:20

2 Answers2

2

Your example can be changed to working C++ code fairly easily:

template <typename T, typename Fun> auto map(T t, Fun f) {
  std::vector<typename T::value_type> ordinals;
  std::transform(t.begin(), t.end(), std::back_inserter(ordinals), f);
  return ordinals;
}

This is neither very idiomatic nor optimized for performance, but it works. (Pre-resize the vector instead of using std::back_inserter and pass t by reference for a big performance boost.)

Edit: You can add reduce as follows:

template <typename T, typename R, typename Fun> auto reduce(T t, R init, Fun f) {
  return std::transform(t.begin(), t.end(), init, f);
}

And then combine them: reduce(map(x, [](auto a) {...}), 0, [](int& a, auto b) {...})

The obvious caveat: This is going to be really slow. These functions are fun to learn template concepts, but in practice, ranges or old-style std::reduce/... will be much, much faster.

Edit 2: If you want to use map as a member function, just make a wrapper class:

template<class T>
struct Wrapper {
  T d;
  template <typename Fun> auto map(Fun f) {
    std::vector<typename T::value_type> ordinals;
    std::transform(d.begin(), d.end(), std::back_inserter(ordinals), f);
    return wrap(ordinals);
  }
  template <typename R, typename Fun> auto reduce(R init, Fun f) {
    return wrap(std::transform(d.begin(), d.end(), init, f));
  }
};
template<class T> Wrapper<T> wrap(const T& t) { return {t}; }
template<class T> Wrapper<T> wrap(T&& t) { return {t}; }

Then you can do wrap(my_vec).map(...).reduce(..., ...)

for loops would be faster here, because you're doing a lot of unnecessary copying: you're unnecessarily creating a new vector in map and copy data lots of times (std::back_inserter, pass-by-value).

user3684240
  • 1,420
  • 2
  • 12
  • 18
  • Thanks, it looks like this does it quite nicely for map. If I wanted to expand it to then pass it into a reduce, like the new c++20 ranges library, what would I need to do? Create some form of new datatype from which I can call reduce? – Samuel Feb 08 '22 at 03:18
  • Thanks, I admit that I'll need a little time to think about your reduce implementation. Do you think that I would be able to adapt it to be called like `map(x, [](auto a) {...}).reduce(0, [int& a, auto b] {...})` if I created a new type for the transform result? About the speed (it isn't really a concern for me, this is mainly curiosity) but if these where implemented using for loops, do you know if they'd be considerably faster? – Samuel Feb 08 '22 at 03:37
  • @Samuel Responded to you in an edit of the answer. – user3684240 Feb 08 '22 at 15:48
  • If there's anything I can do to help you understand the reduce implementation, let me know! – user3684240 Feb 08 '22 at 15:48
  • Thanks, things are very clear now! Just before I accept the answer, could something like that be done with the kotlin `apply` example(set many properties at the same time)? – Samuel Feb 08 '22 at 16:32
  • I believe that I understand everything now, thanks for the help! Let can be implemented as a fuction which receives an object + a lambda expression using templates. – Samuel Feb 08 '22 at 17:14
1

I'd like to know if it would be possible to do something like:

int[] arr = {1, 2, 3};
auto d_arr = map(arr, [](int x){return x*2})

"map" in this sense is called "transform" in C++ standard library. A working example:

auto double_ = [](auto x){ return x*2; }; // you may also use a normal function
auto doubled_view = std::views::all(arr)
                  | std::views::transform(double_);

to be able to chain functions like reduce with map.

You can reduce ranges. Unfortunately, the C++20 standard ranges don't come with a reduce function, so one has to use either another implementation of ranges, or use the iterator based functions instead. Example of latter:

auto sum = std::reduce(
     std::execution::unseq, // optional execution policy
     doubled_view.begin(),
     doubled_view.end(),
     0
     // you can use a functor to do something other than add
     );

conversions to std::vector

You can also create containers from ranges:

std::vector doubled_vector(
    doubled_view.begin(), doubled_view.end());

Note that creating containers for intermediate stages of range views can have unnecessary overhead.

eerorika
  • 232,697
  • 12
  • 197
  • 326