使用C ++模板实现Haskell的`map`函数的问题

时间:2017-12-10 20:01:44

标签: c++ templates haskell polymorphism

我喜欢使用Haskell,但我被迫使用C ++进行学校作业。我正在为C ++编写自己的库来模拟Haskell的Prelude函数,所以如果我愿意的话,我可以用C ++编写一个更简洁和功能更强的样式(repo on GitHub)。

我遇到的一个问题是实现对列表进行操作的map等函数。在Haskell中,String等同于[Char],因此您可以在采用列表的函数中使用字符串。在C ++中,std::string std::vector<char>相同,因此我必须编写多个版本的函数来执行std::stringstd::vector<Type>。这适用于filtertail等函数,因为它们的输入和输出是相同的类型。但是对于map,我需要能够将int s的向量转换为char s,或将string转换为bool s的向量。

尝试运行简单的猪拉丁转换器(pigLatin.cpp on GitHub)时,unwords函数失败,因为map无效。

examples/pigLatin.cpp:20:29: error: no matching function for call to 'unwords'
  std::string translation = unwords(map(pigLatin, words(input)));
                            ^~~~~~~
examples/../prelude.hpp:591:15: note: candidate function not viable: no known conversion from 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >') to 'const std::vector<std::string>'
      (aka 'const vector<basic_string<char, char_traits<char>, allocator<char> > >') for 1st argument
  std::string unwords(const std::vector<std::string> &xs) {
              ^
1 error generated.

如何编写map函数,使其行为类似于Haskell(map on Hackage)中的函数:

map :: (a -> b) -> [a] -> [b]

我不太了解C ++模板的细微差别来解决这个问题。这是我到目前为止(map from prelude.hpp on GitHub):

// map :: (a -> b) -> [a] -> [b]
template <typename Function, typename Input, typename Output>
std::vector<Output> map(const Function &f, const std::vector<Input> &xs) {
  const int size = xs.size();
  std::vector<Output> temp;
  for (int i = 0; i < size; ++i) {
    temp.push_back(f(xs[i]));
  }
  return temp;
}

// map :: (String -> a) -> String -> [a]
template <typename Function, typename Output>
std::vector<Output> map(const Function &f, const std::string &xs) {
  const int size = xs.size();
  std::vector<Output> temp;
  for (int i = 0; i < size; ++i) {
    temp.push_back(f(xs[i]));
  }
  return temp;
}

// map :: (a -> String) -> [a] -> String
template <typename Function, typename Input>
std::string map(const Function &f, const std::vector<Input> &xs) {
  const int size = xs.size();
  std::string temp;
  for (int i = 0; i < size; ++i) {
    temp += f(xs[i]);
  }
  return temp;
}

// map :: (String -> String) -> String -> String
template <typename Function>
std::string map(const Function &f, const std::string &xs) {
  const int size = xs.size();
  std::string temp;
  for (int i = 0; i < size; ++i) {
    temp += f(xs[i]);
  }
  return temp;
}

1 个答案:

答案 0 :(得分:8)

在此声明中:

template <typename Function, typename Input, typename Output>
std::vector<Output> map(const Function &f, const std::vector<Input> &xs);

Output是一个非推断的上下文。编译器将从提供的参数中推导出FunctionInput的类型,但不能推导出Output - 必须明确提供它。那不会发生。

您要做的是自己计算OutputFunctionInput的函数关系。使用C ++ 17编译器/库,即std::invoke_result_t(在C ++ 11上,使用result_of)。那就是:

template <typename Function, typename Input,
    typename Output = std::invoke_result_t<Function const&, Input const&>>
std::vector<Output> map(const Function &f, const std::vector<Input> &xs);

这是一个默认的模板参数,但由于用户实际上不会提供它,因此将使用默认参数,这就是您想要的。现在这也不完全正确,因为invoke_result_t可以返回一些你不能放在vector中的东西(比如参考)。所以我们需要std::decay它。此外,您需要保留输出向量,因为我们事先知道它的大小:

template <typename Function, typename Input,
    typename Output = std::decay_t<std::invoke_result_t<Function&, Input const&>>>
std::vector<Output> map(Function&& f, const std::vector<Input> &xs)
{
    std::vector<Output> res;
    res.reserve(xs.size());
    for (auto&& elem : xs) {
        res.push_back(f(elem));
    }
    return res;
}

现在,如果您希望这能够获得string并返回vector<X> 一个string,那么现在变得相当复杂。你不能在C ++中重载返回类型,所以提供这两个重载是不正确的。它恰好适用于您的情况,因为string --> vector<X>重载将因X无法推断而被删除。但是一旦你解决了这个问题,你就会遇到这个问题。