Я пытаюсь использовать необычные функции -std = c ++ 14 для реализации комбинатора map, который вы видите на функциональных языках (не путать с std :: map). Моя конечная цель — написать заголовок «шаблона фасада» для функционального программирования, который позволит мне забыть о побочных эффектах и итераторах большую часть времени. Я нашел сообщение от единомышленника в https://gist.github.com/phatak-dev/766eccf8c72484ad623b . Мадхукара версия карты выглядит так
template <typename Collection,typename unop>
Collection map(Collection col,unop op) {
std::transform(col.begin(),col.end(),col.begin(),op);
return col;
}
Кажется, он работает идеально, если вы не просите что-то глупое, но тип возвращаемого значения должен совпадать с набором входных данных. Моя попытка обобщить наличие домена и диапазона различных типов заключается в следующем:
template <typename Collection, typename function>
auto map(function f, Collection c) {
auto result;
std::transform(c.begin(),c.end(),result.begin(),f);
return result;
}
Это не компилируется, но, надеюсь, кому-то понятно, что я пытаюсь сделать … Я хочу инициализировать пустой-же-тип-контейнера-as-c выходных-типа-f, а затем положить f(c[i])
в этом. Компилятор жалуется, что объявление ‘auto result’ не имеет инициализатора, но я не знаю, как запросить пустое значение независимо от того, что из этого. Есть ли способ настроить эту строку, чтобы она делала то, что я пытаюсь сделать? Я никогда раньше не пытался сделать что-нибудь такое экзотическое с auto, поэтому любые дополнительные предложения приветствуются.
Спасибо!
Джон
Редактировать: вот пример, который, мы надеемся, чувственный:
auto first_letter = [](string s) { return s[0]; }
vector<string> words;
words.push_back("hello"); words.push_back("world");
vector<char> first_letters = map(first_letter, words); // {'h','w'}
Изменить 2: Вот еще один подход, который использует тяжеловесную библиотеку «потоков» (не путать с потоками ввода-вывода) для реализации «шаблона итератора», такого как потоки Java:
http://jscheiny.github.io/Streams/
Java-подход:
http://tutorials.jenkov.com/java-collections/streams.html
Такой потоковый подход обеспечивает большую свободу выбора типов контейнеров (как, кажется, поддерживают несколько респондентов) и ленивую оценку.
Просто используйте повышение :: адаптеры :: tranformed:
#include <boost/range/adaptor/transformed.hpp>
template <typename Collection, typename function>
auto map(function f, Collection c) {
return c | boost::adaptors::transformed(f);
}
С этим диапазоном — вы можете создать любой контейнер, который вы хотите.
char first_letter(string s) { return s[0]; }
vector<string> words;
words.push_back("hello"); words.push_back("world");
auto transformed_range = map(first_letter, words);
vector<char> first_letters(begin(transformed_range ), end(transformed_range ));
Если вы настаиваете на map
функция, возвращающая контейнер, а не диапазон — добавьте еще один параметр в этот шаблон функции:
#include <boost/range/adaptor/transformed.hpp>
template <typename Result, typename Collection, typename function>
auto map(function f, Collection c) {
auto transformed_range = c | boost::adaptors::transformed(f);
return Result(begin(transformed_range), end(transformed_range));
}
char first_letter(string s) { return s[0]; }
vector<string> words;
words.push_back("hello"); words.push_back("world");
vector<char> first_letters = map<vector<char>>(first_letter, words);
Но если вы действительно настаиваете на том, чтобы вести себя точно так, как вы хотите — у вас должны быть некоторые черты, знающие, как преобразовать тип коллекции в другой тип коллекции с преобразованным значением.
Первый — способ получить new_value_type:
template <typename Function, typename OldValueType>
struct MapToTransformedValue
{
using type = decltype(std::declval<Function>()(std::declval<OldValueType>()));
};
Общая черта:
template <typename Function, typename Container>
struct MapToTransformedContainer;
Самый простой случай — для std::array
:
// for std::array
template <typename Function, typename OldValueType, std::size_t N>
struct MapToTransformedContainer<Function, std::array<OldValueType, N>>
{
using value_type = typename MapToTransformedValue<Function, OldValueType>::type;
using type = std::array<value_type, N>;
};
За std::vector
— немного сложнее — вам нужно предоставить новый распределитель, для распределителей std — вы можете использовать его шаблон повторной привязки:
// for std::vector
template <typename Function, typename OldValueType, typename OldAllocator>
struct MapToTransformedContainer<Function, std::vector<OldValueType, OldAllocator>>
{
using value_type = typename MapToTransformedValue<Function, OldValueType>::type;
using allocator = typename OldAllocator::template rebind<value_type>::other;
using type = std::vector<value_type, allocator>;
};
Итак, ваша функция будет выглядеть следующим образом:
template <typename Collection, typename function>
auto map(function f, Collection c)
{
using NewCollectionType = typename MapToTransformedContainer<function, Collection>::type;
auto transformed_range = c | boost::adaptors::transformed(f);
return NewCollectionType (begin(transformed_range), end(transformed_range));
}
Сейчас — твой main()
по желанию:
char first_letter(std::string const& s) { return s[0]; }
int main() {
std::vector<std::string> words;
words.push_back("hello"); words.push_back("world");
auto first_letters = map(first_letter, words);
std::cout << first_letters[0] << std::endl;
}
Остерегайтесь, что для других контейнеров, где value_type
состоит из Key,Value
пара — как std::map
, std::set
(и их неупорядоченные _… братья и сестры) вы должны определить другую специализацию MapToTransformedContainer
…
Я бы использовал тот факт, что большинство контейнеры есть конструктор, принимающий пару итераторов.
#include <boost/iterator/transform_iterator.hpp>
template <typename Function, typename Collection>
struct map_to
{
Function f;
const Collection& c;
template <typename T>
operator T() &&
{
using std::begin; using std::end;
return { boost::make_transform_iterator(begin(c), f)
, boost::make_transform_iterator(end(c), f) };
}
};
template <typename Function, typename Collection>
map_to<Function, Collection> map(Function f, const Collection& c)
{
return { f, c };
}
тесты:
int main()
{
std::vector<std::string> words;
words.push_back("hello");
words.push_back("world");
auto first_letter = [](std::string s) { return s[0]; };
std::vector<char> v = map(first_letter, words);
std::set<char> s = map(first_letter, words);
std::forward_list<char> f = map(first_letter, words);
}
Просто для удовольствия, вот мой путь к этой проблеме:
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
template <template<typename...> class Collection>
struct mapper {
template<typename function, typename T, typename... Rest>
static auto map(function f, const Collection<T, Rest...>& c) {
Collection<decltype(f(*c.begin()))> result;
std::transform(c.begin(),c.end(),std::inserter(result, result.end()),f);
return result;
}
};
int main()
{
// Example 1
std::vector<int> v{0, 1};
auto fv = mapper<std::vector>::map([](const auto& val) { return val + 0.1f; }, v);
for (const auto& f : fv) { std::cout << f << " "; }
std::cout << "\n";
// Example 2
std::set<float> m{1, 2, 3, 4};
auto fm = mapper<std::set>::map([](const auto& val) { return static_cast<int>(val / 2.0f); }, m);
for (const auto& f : fm) { std::cout << f << " "; }
}
Обратите внимание, что это будет работать только до тех пор, пока вы довольны значениями по умолчанию для всего, кроме параметра типа выходного контейнера.
У меня была та же самая задача, и я был в порядке с компромиссом по функциональности, если это означало уменьшенный шаблон.
Вот что я закончил (может не работать с контейнерами, которые используют более одного явного аргумента или чьи аргументы шаблона не могут быть выведены тривиально, но вы согласитесь, что он чист).
using std::vector;
template<typename Src, typename Dst, template<class, typename ...> typename Container>
Container<Dst> fmap(Container<Src>& container, Dst(*f)(Src)) {
Container<Dst> result;
result.reserve(container.size());
std::transform(container.begin(), container.end(), std::back_inserter(result), f);
return result;
}
int main() {
vector<int> a = {1, 2, 3};
auto f = [](int x) -> double { return (x + 1)/2; };
vector<double> b = fmap(a, +f);
for (auto x: b) {
std::cout << x << std::endl;
}
return 0;
}
Основным требованием для меня было не делать его уродливым, и он не должен использовать повышение.
Изменить: Это также будет вести себя глупо (читай: не компилировать), если вы попытаетесь передать вектор с нестандартным распределителем, скажем.