【问题标题】:Haskell typeclasses and C++ template classesHaskell 类型类和 C++ 模板类
【发布时间】:2010-12-10 16:53:21
【问题描述】:

是否可以使用 C++(或 C#)模板来模拟 Haskell 的类型类功能?

这样做是否有意义或有任何回报?

我试图在 C++ 中创建一个 Functor 类,但我做不到。我试过这样的事情:

#include <iostream>
using namespace std;

//A function class to make types more readable
template <class input, class output> class Function {
private:
  output (*ptrfunc )(input);
public:
  Function(output (* ptr)(input)) {
    ptrfunc = ptr;
  }
  output call(input x) {return  (*ptrfunc)(x);}
  output operator() (input x) { return call(x);}
};


//the functor "typeclass"
template <class a> class Functor{
public:
  template <class b> Functor<b> fmap(Function<a,b> func);
};

// an container type to be declared "instance" of functor:
template <class a> class List : public Functor<a> { 
private:
  a * ptrList;
  int size;
public:
  List(int n) {  //constructor;
    ptrList = new a[n]; 
    size = n;
  }
  List(List<a> const& other) { //copy constructor
    size = other.size;
    ptrList = new a[size];
    for(int i = 0; i<size; i++)
      (*this)[i] = other[i];
  }
  ~List() { delete ptrList;} //destructor
  a& operator[](int i) { return ptrList[i];} // subscript operator just for easy notation
  const a& operator[](int i) const { return ptrList[i];}// subscript operator just for easy notation

  template <class b> List<b> fmap(Function<a,b> func) { //"instance" version of fmap
    List<b> temp(size);
    for(int i = 0; i < size; i++)
      temp[i] = func((*this)[i]);
    return temp;
  }
};


int test(int k) { return 2 * k;}

int main(void) {
  Function<int, int> func(&test);
  List<int> lista(10);
  for(int i = 0; i < 10; i++)
    lista[i] = i;
  List<int> lista2(lista.fmap(func));
  for(int i = 0; i < 10; i++)
    cout << lista2[i] << " ";
  cout << endl;
  return 0;
}

它做了它应该做的事情,但是在 C++ 中使用这种模式是否有意义?真的和haskell中的模式一样吗:

data List a = -- some stuff 

class  Functor f  where
fmap :: (a -> b) -> f a -> f b

instance (Functor List) where
-- some stuff    

对我来说似乎不是一回事,因为Functor ff 是一种 * -&gt; * 类型的构造函数,而在我上面的定义中 Functor&lt;a&gt;a 不是模板a&lt;something&gt;,但“包含”数据类型本身。

有办法解决吗?更重要的是:尝试将这种模式复制到 C++ 是否有意义?在我看来,C# 比 C++ 更类似于函数式编程风格。有没有办法在 C# 中做到这一点?

【问题讨论】:

  • 一个好的经验法则是“不,假装用你正在使用的其他语言编程是不值得的”。如果您想编写 Haskell 代码,请为 Haskell 编译器编写它。只要你在写 C++ 代码,你最好写惯用的 C++
  • 也许osl.iu.edu/~kyross/pub/20040929-type-class-slides.pdfciteseerx.ist.psu.edu/viewdoc/… 会有所帮助。但是,在您的 Functor 情况下,基类绝对没有意义(因为不能将 fmap 声明为 virtual)。
  • 但是是的,您可以使用模板编写通用仿函数。虽然这样做的风格会与 Haskal 中的略有不同。
  • 我只想指出,您可能会将 C++ 程序员与 Functor 一词混淆,因为该术语在 C++ 中被滥用为表示功能对象(在某种意义上是一种特定类型的 Functor)。在 haskell 中,Functor 表示分类 Functor 而不是 C++ Functor(功能对象)。
  • 您可能还想阅读 C++ 概念与提升 BCCL:stackoverflow.com/questions/1352571/…。这不是一个详尽的答案,但我相信它有助于澄清删除 C++0x 概念的问题。

标签: c# c++ design-patterns haskell typeclass


【解决方案1】:

是否可以使用 C++(或 C#)模板来模拟 Haskell 的类型类功能?

我对 C++ 模板的了解不够,无法回答这个问题。不过我可以谈谈 C# 泛型类型。

简短的回答是,不。 “高级”类型类的 Haskell 系统比 C# 中的泛型类型系统更强大。

此时,对于仍在阅读本文但不熟悉 Haskell 的任何读者,简要讨论一下“高级类型”的含义可能会很有用。在 C# 中,您可以这样做:

interface IEnumerable<T> { ... }

“通用”类型“IEnumerable of one type parameter”本身并不是真正的“类型”,它是一种模式,您可以从中构造无限多的新类型,方法是用类型参数(“int”)代替类型参数(“T”)。从这个意义上说,它比普通类型“更高”。

您可以将约束放在泛型类型的类型参数上:

class C<T> where T : IEnumerable<int>

通用类型 C 可以使用任何类型参数构造,只要类型参数是可通过引用或装箱转换隐式转换为 IEnumerable&lt;int&gt; 的类型。

但 Haskell 类型系统比这更进一步。它支持“类型类”,您可以对 T 施加的约束类似于“T 上定义了一个相等运算符”。在 C# 中,运算符被定义为 static 方法,并且对于静态方法没有类似的 interface。我们无法在 C# 中基于任意静态方法泛化多种类型。

Haskell 通常给出的一个例子是“monad”模式。在 C# 表示法中,假设我们有一个类型:

class MyMonad<T>
{
    public static MyMonad<TOut> Bind<TIn, TOut>(MyMonad<TIn>, Func<TIn, MyMonad<TOut>>) { ... }
    public MyMonad(T t) { ... }
}

单子只是一种模式; monadic 类型是任何泛型类型,因此它具有静态泛型方法 Bind 和与上述模式匹配的构造函数。在 Haskell 中,您可以使用更高的类型来描述该模式;在 C# 中,我们在类型系统中没有用于泛化静态方法和构造函数之类的工具。

或者,您可能会说使用实例绑定器会更惯用:

class MyMonad<T>
{
    public MyMonad<TOut> Bind<TOut>(MyMonad<T>, Func<T, MyMonad<TOut>>) { ... }
    public MyMonad(T t) { ... }
}

这有帮助吗?不,即使把构造函数的问题放在一边,我们也不能想出一个捕捉这种模式的接口。我们可以试试:

interface IMonad<T>
{
    public IMonad<TOut> Bind<TOut>(IMonad<T>, Func<T, IMonad<TOut>>);
}

但是这是不对的。这就是说 monad 是接受一个 monad 和一个返回 monad 并返回一个 monad 的函数的东西。这意味着您可以拥有IMonad&lt;T&gt; 的两个实现,例如Maybe&lt;T&gt;Sequence&lt;T&gt;,然后拥有一个接受序列并返回可能的活页夹!这没有任何意义;我们要捕捉的模式是

highertype Monad makes a pattern with TheImplementingType<T> like
{
    public TheImplementingType<TOut> Bind<TOut>(TheImplementingType<T>, Func<T, TheImplementingType<TOut>>);
}

但我们无法用 C# 来表达。

让我们考虑一下您的 Functor 示例。在 C# 中,我们可能有一个类型

class List<T> 
{
    public static List<TOut> Map<TIn, TOut>(Func<TIn, TOut> mapper, List<TIn> list) 
    { ... }

或者,也许更习惯地说,一个实例方法:

class List<T> 
{
    public List<TOut> Map<TOut>(Func<T, TOut> mapper) 
    { ... }

或者,更惯用的说法是,我们可以将静态方法作为扩展方法。 (事实上​​,这个方法确实存在于 C# 的序列运算符库中;它可以通过将 IEnumerable&lt;T&gt; 上的“Select”与“ToList”组合来构建)。

随便。没关系。关键是在您的 Haskell 代码中:

class  Functor f  where 
fmap :: (a -> b) -> f a -> f b 

您可以说“任何公开符合上述模式的映射操作的泛型类型都被称为'Functor'”,然后您可以创建采用 Functor 的方法。我们没有任何方法可以在 C# 中在用户级别概括“所有提供映射操作的类型”。

为了绕过类型系统的这个限制,我们选择了一些最强大的高级类型并将它们直接构建到语言中。语言本身识别更高的类型,如序列模式(在 foreach 循环处理中)、广义 monad 模式(在查询理解中;“SelectMany”是任意 monad 上的“Bind”)、延续模式(在“await”中C# 5)、"Maybe" monad(可空值类型)等等。

所以为了解决你的特定问题,是的,没问题,制作投影的概念没有在类型系统中捕获,而是在中捕获具有 LINQ 查询理解的语言。如果你说

from x in y select z

然后 any 类型的表达式 y 具有 Select 方法,该方法采用 y 和从 x 到 z 的映射将起作用。该模式已内置到 C# 语言中。但是,如果您想描述一些 other “更高”的模式,那你就不走运了。

如果在类型系统中有一个工具来描述更高的类型,那就太好了,但更有可能的是,我们将保持类型系统不变,并根据需要将更多模式融入语言中。

这个问题描述了我经常看到在 C# 中模拟高阶类型的尝试:

Why does this generic constraint compile when it seems to have a circular reference

这里的想法是,开发人员希望开发一种“动物”类型:

abstract class Animal
{
    public abstract void MakeFriends<T>(T newFriend)
    where T : THISTYPE;
}

虚构的“where T : THISTYPE”试图理解猫只能与另一只猫交朋友,狗只能与另一只狗交朋友的想法,等等。 (暂时忽略这样一个事实,即暗示 MakeFriends 在形式参数类型上具有虚拟协变的这种模式不是类型安全的,因此可能会违反 Liskov 替换原则。)这个概念可以在更高的类型中表达,但不能在更高的类型中表达。 C# 类型系统。人们有时会使用这样的模式:

abstract class Animal<T> where T : Animal<T>
{
    public abstract void MakeFriends(T newFriend);
}
class Cat : Animal<Cat>
{
    public override void MakeFriends(Cat newFriend){ ... }
}

但是,这实际上并没有强制执行所需的约束,因为当然没有什么可以阻止您说:

class Dog : Animal<Cat>
{
    public override void MakeFriends(Cat newFriend){ ... }
}

现在狗可以和猫成为朋友,这违背了动物作者的意图。 C# 类型系统根本不足以表示您可能想要的所有类型的约束。您必须使用更高的类型才能使其正常工作。

【讨论】:

  • 总体+1,但是:'...“也许”monad(在可空值类型中)...' 倒退了一大步,我真的不明白它是如何“适应”更高的。
  • @pst:那么,如何将“可能”类型添加到已经具有可为空引用类型的语言中?作为象牙塔的 Haskell 纯粹主义者,这一切都很好,但我们拥有数百万的安装基础,不能随意在类型系统中引入全新的概念。可空值类型远非完美;它们并非完美无缺。考虑到该语言和运行时的先前版本的存在,它们旨在成为一种合理的折衷方案。
  • @Eric Lippert 请参阅 Scala 和 Option。 Scala 支持可为空的类型。我不喜欢Nullable&lt;T&gt; 的一件事(以及为什么我发现它没有“替换”Maybe)是T 被限制为一个值类型并且没有适当的LINQ/扩展支持——它太具体了。二分法没有被删除——只是一些语法来增加浮动的空值数量。耶。
  • 当他询问 C++ 和 Haskell 时,我不明白继续讨论 .NET 泛型的意义,不要假设因为 .net 泛型不支持适用于的更高种类的多态性C++ 也是如此。 C++ 确实支持使用模板模板参数的某种更高种类的多态性(是的,我说的是模板模板),但它在 C++ 中几乎是无用的功能,因为它与普通模板类型参数完全不同并且缺乏类型推断,几乎不支持对它们进行类型推导。
  • @snk_kid:再次阅读问题的第一段,包括括号中的那段。问题是关于 C++ 和 C# 类型系统。我注意到我没有对 C++ 模板类型系统做任何假设或陈述。
【解决方案2】:

C++0x 将引入与 Haskell 的类型类非常相似的 概念,但该功能被否决了。再耐心等待十年,他们可能最终会进入这门语言。

【讨论】:

  • 此外,我想提一下,概念/类型类“只是”类型的类型系统。没有它们,C++0x 几乎同样强大。没有概念,一些类型检查就会被简单地延迟(这可能会导致臭名昭著的模板错误消息)。
  • 在使用概念时验证模板的正确性要容易得多。 “某些类型检查只是延迟了”并不是真的。这更像是“根本没有完成某些类型检查”。例如vector&lt; auto_ptr&lt;U&gt; &gt; x; 可能会默默地“工作”并导致未定义的行为。对模板参数的适当概念约束将要求 auto_ptr&lt;U&gt; 是 CopyAssignable 或任何需要的。
  • 你根本不能做的是在没有概念的情况下检查 C++ 中的模板定义本身。比如,一个常见的例子,template&lt;typename ForwardIterator&gt; ForwardIterator find(ForwardIterator begin, ForwardIterator end) { ... begin + 1 ... }。假设你有这样的身体。如果所有用户都将向量迭代器传递给find(这是允许的),您将永远不会注意到您的模板是错误的。但实际上它错误的,因为 ForwardIterators 在概念上没有op+
  • 只是顺便说一下 - 10 年后,概念就在这里。
【解决方案3】:

来自this blog post 在 Haskell 和 C++ 上的作者:

在 C++ 中进行编译时计算的能力是发现的,而不是内置在语言中。

虽然可以在 C++ 模板中模拟许多 Haskell 功能,但翻译后的语法确实很难看。这主要是因为 C++ 中的元编程是一个意外,而不是最初构思的特性。

【讨论】:

【解决方案4】:

尽管 C# 和 C++ 在帖子中被单独列出,但与 Scala(2.8,目标是 JVM)进行比较可能会很有趣。 Scala 与 C# 非常相似,因为它是一种强/静态的单调度“OO”语言——但它具有比 C#3/4 更强大的类型系统(但是,这些特性并不是完全重叠的)。

我对 Haskell 不熟悉,但多年来我阅读了各种文章并听到了许多争论,我相信您正在寻找的类型类功能可以通过 implicits (an example) 派生,即使不是Haskell 中的惯用语。

另一方面,F#,一种“函数式语言”,对类型类构造的支持很差(或者不存在?再说一次,这不是我的领域,但我相信不可能在 F# 中创建 Monad 类型)。发挥语言的优势绝对值得。

编码愉快。

【讨论】:

  • F# 没有类型类并且不支持更高种类的多态性,就像 .NET 通常的情况一样,但这并不意味着没有它们就不能编写 monad,它只是意味着你不能对各种 Monad 进行抽象,从而在它们之上编写通用代码。但是,您可以编写特定的 monad 类型,并且 F# 确实为此提供了一些语法支持。
  • @snk_kid 感谢您的澄清。
【解决方案5】:

也许我在这里遗漏了一些 Haskell 元级别,但您的实际示例,如果没有声明,使用 STL 看起来像这样:

struct Test : public std::unary_function<Foo,SomeResult> {
    SomeResult operator()( const Foo& foo ) const;
};

std::vector<Foo> foos;
...

std::vector<SomeResult> results;
std::transform( foos.begin(), foos.end(), results.begin(), Test() );
....

【讨论】:

  • 正如我在另一篇文章中提到的,Functor 的含义与 C++ 中使用的滥用术语完全不同,它应该是范畴论中的函子,而不是 C++ 函子。
猜你喜欢
  • 2016-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多