将Haskell的类型系统与C#进行比较,寻找类似物

我是一个很新的Haskell编程。 我正在尝试处理它的类,数据,实例和newtype。 这是我所理解的:

data NewData = Constr1 Int Int | Constr2 String Float 

与(Java或C#)大致相同:

 class NewData { private int a, b; private string c; private float d; /* get'ers and set'ers for a, b, c and d ................ */ private NewData() { } private NewData(int a, int b) { this.a = a; this.b = b; } private NewData(string c, float d) { this.c = c; this.d = d; } public static Constr1(int a, int b) { return new NewData(a, b); } public static Constr2(string c, float d) { return new NewData(c, d); } } 

 class SomeClass a where method1 :: [a] -> Bool 

对于

 interface SomeInterface { public bool method1(List someParam); } // or abstract class SomeClass { public abstract bool method1(List someParam); } 

 instance SomeClass Int where method1 a = 5 == head a -- this doesn't have any meaning, though, but this is correct 

对于

  class SomeClassInstance: SomeClass { public bool method1(List param) { return param.first == 5; // I don't remember the method's name exactly, it doesn't matter } } 

这些都是正确的吗? 那么newtype,我怎样才能用C#或Java来表示呢?

正如其他人所说,它更像是一个有区别的联盟 – 这是一个模糊的结构,只有C / C ++程序员可能会听说过。

您可以通过为Haskell的“类型”提供一个抽象基类来为OO语言模拟这一点,并为每个Haskell的“构造函数”提供一个具体的子类。 特别是,您的代码片段表示每个 NewData对象都有四个字段; 这是不正确的。 你可以这样做:

 data Stuff = Small Int | Big String Double Bool 

现在,如果我写Small 5 ,这是一个Stuff值,里面只有1个字段。 (它占用了大量的RAM。)但是,如果我做Big "Foo" 7.3 True ,这也是 Stuff类型的值,但它包含3个字段(并占用了大量的RAM)。

请注意,构造函数名称本身是数据的一部分。 这就是为什么你可以做类似的事情

 data Colour = Red | Green | Blue 

现在有三个构造函数,每个构造函数都有零字段。 构造函数本身就是数据。 现在,C#允许你这样做

 enum Colour {Red, Green, Blue} 

但那真的只是说

 Colour = int; const int Red = 0; const int Green = 1; const int Blue = 2; 

请特别注意,您可以说

 Colour temp = 52; 

相比之下,在Haskell中, Colour类型的变量只能包含RedGreenBlue ,而这些变量不是任何整数。 如果愿意,可以定义一个函数它们转换为整数,但这不是编译器存储它们的方式。

你对getter和setter的评论说明了这种方法的缺陷; 在Haskell,我们通常不会担心吸气剂和二传手。 简单地定义类型就足以创建该类型的值并访问其内容。 它有点像C# struct ,所有字段都标记为public readonly 。 (当我们担心吸气剂时,我们通常将它们称为“投影function”……)

在OO中,您使用类进行封装。 在Haskell中,您可以使用模块执行此操作。 在模块内部,一切都可以访问所有内容(很像类可以访问自身的每个部分)。 您可以使用导出列表来说明模块的哪些部分对外部公开。 特别是,您可以将类型名称设为public,同时完全隐藏其内部结构。 然后,创建或操作该类型值的唯一方法是从模块公开的函数。


你问过newtype

好的, newtype关键字定义了一个新类型名称,它实际上与旧类型相同,但类型检查器认为它是新的和不同的东西。 例如, Int只是一个正常数字。 但如果我这样做

 newtype UserID = ID Int 

现在UserID是一种全新的类型,完全与任何东西无关。 但是在幕后,它真的只是好老Int另一个名字。 这意味着您不能在需要Int地方使用UserID – 并且您不能在需要UserID地方使用Int 。 因此,您不能将用户ID与其他随机数混淆,因为它们都是整数。

你可以用data做同样的事情:

 data UserID = ID Int 

但是,现在我们有一个无用的UserID结构, UserID包含一个指向整数的指针。 如果我们使用newtype那么UserID 一个整数,而不是指向整数的结构。 从程序员的角度来看,这两个定义都是等价的; 但在引擎盖下, newtype更有效率。

(次要的挑选:实际上,你需要说的是相同的

 data UserID = ID !Int 

这意味着整数字段是“严格的”。 不要担心这个。)

考虑Haskell数据结构的另一种方法是在C中使用这种“区别联合”结构:

 typedef enum { constr1, constr2 } NewDataEnum; typedef struct { NewDataEnum _discriminator; union { struct { int a,b; } _ConStr1; struct { float a,b; } _ConStr2; } _union; } NewData; 

请注意,为了访问Haskell类型中的任何Int或Float值,您必须模式匹配构造函数,这对应于查看_discriminator字段的值。

例如,这个Haskell函数:

 foo :: NewData -> Bool foo (ConStr1 ab) = a + b > 0 foo (ConStr2 ab) = a * b < 3 

可以作为这个C函数实现:

 int foo(NewData n) { switch (n._discriminator) { case constr1: return n._union._ConStr1.a + n._union._ConStr1.b > 0; case constr2: return n._union._ConStr2.a * n._union._ConStr2.b < 3; } // will never get here } 

为了完整性,以下是使用上述C定义的构造函数ConStr1的实现:

 NewData ConStr1(int a, int b) { NewData r; r._discriminator = constr1; r._union._ConStr1.a = a; r._union._ConStr1.b = b; return r; } 

Java和C#没有对工会的直接支持。 在C联合中,联合的所有字段在包含结构中被赋予相同的偏移量,因此联合的大小是其最大成员的大小。 我已经看过C#代码,它不用担心浪费空间而只是简单地使用struct进行联合。 这是一篇MSDN文章 ,讨论了如何获得C风格联合所具有的重叠效果。

代数数据类型在很多方面与对象互补 - 一个很容易做到的事情很难用另一个做 - 所以它们不能很好地转换为OO实现也就不足为奇了。 对“表达问题”的任何讨论通常都强调了这两个系统的互补性。

对象,类型类和algrbraic数据类型可以被认为是通过跳转表有效地转移控制的不同方式,但是这些表的位置在每种情况下都是不同的。

  • 对象跳转表是对象本身的隐式成员(即_vptr
  • 对于类型类,跳转表作为函数的单独参数传递 - 即它与数据分开
  • 使用代数数据类型,跳转表直接编译到函数中 - 即上例中的switch语句

最后,应该强调的是,在Haskell中,您指定的代数数据类型(ADT)的实现细节很少。 区分联合构造是以具体术语考虑ADT的有用方法,但Haskell编译器不需要以任何特定方式实现它们。

要使总和类型像

data NewData = Constr1 Int Int | Constr2 String Float

我通常在c#中执行以下操作

 interface INewDataVisitor { R Constr1(Constr1 constructor); R Constr2(Constr2 constructor); } interface INewData { R Accept(INewDataVisitor visitor); } class Constr1 : INewData { private readonly int _a; private readonly int _b; Constr1(int a, int b) { _a = a; _b = b; } int a {get {return _a;} } int b {get {return _b;} } R Accept(INewDataVisitor visitor) { return visitor.Constr1(this); } } class Constr2 : INewData { private readonly string _a; private readonly float _b; Constr2(string a, float b) { _a = a; _b = b; } string a {get {return _a;} } float b {get {return _b;} } R Accept(INewDataVisitor visitor) { return visitor.Constr2(this); } } 

这在类型安全方面并不完全相同,因为INewData也可以为null ,可能永远不会调用访问者的方法,只返回default(R) ,可能多次调用访问者,或任何其他愚蠢的事情。

一个c#界面就像

 interface SomeInterface { public bool method1(List someParam); } 

在Haskell中真的更像是以下内容:

 data SomeInterface t = SomeInterface { method1 :: [t] -> bool } 

Haskell的数据类型与任何特定的C#构造都不完全相同。 您可以期待的最好的方法是模拟某些function。 最好按照自己的条件理解Haskell类型。 但我会捅它。

我没有方便的C#编译器,但我指的是希望产生接近正确的东西的文档。 我会稍后编辑以修复错误,如果它们被指出给我。

首先,Haskell中的代数数据类型最接近一类OO类而不是单个类。 除了区分具体子类的单个字段之外,父类是完全抽象的。 该类型的所有公共用户必须接受父类,然后通过鉴别器字段执行案例分析,并对鉴别器指示的更具体的子类进行类型转换。

 class NewData { // every piece of NewData may take one of two forms: static enum Constructor { C1, C2 } // each piece of data has a discriminator tag; this is the only structure // they all have in common. Constructor discriminator; // can't construct a NewData directly private NewData() {} // private nested subclasses for the derived types private class Constr1Class : NewData { int a, b; Constr1Class(int a, int b) { this.discriminator = NewData.C1; this.a = a; this.b = b; } } private class Constr2Class : NewData { string c; float d; Constr2Class(string c, float d) { this.discriminator = NewData.C2; this.c = c; this.d = d; } } // A bunch of static functions for creating and extracting // I'm not sure C# will be happy with these, but hopefully it is clear // that they construct one of the derived private class objects and // return it as a parent class object public static NewData Constr1(int a, int b) { return new Constr1Class(a, b); } public static NewData Constr2(string c, float d) { return new Constr2Class(c, d); } // We can't directly get at the members since they don't exist // in the parent class; we could define abstract methods to get them, // but I think that obscures what's really happening. You are expected // to check the discriminator field first to ensure you won't get a // runtime type cast error. public static int getA(NewData data) { Constr1Class d1 = (Constr1Class)data; return d1.a; } public static int getB(NewData data) { Constr1Class d1 = (Constr1Class)data; return d1.b; } public static string getC(NewData data) { Constr2Class d2 = (Constr2Class)data; return d2.c; } public static float getD(NewData data) { Constr2Class d2 = (Constr2Class)data; return d2.d; } } 

毫无疑问,你会批评这是可怕的OO代码。 必然是! Haskell的代数数据类型并不声称是面向对象意义上的对象。 但至少应该让你了解ADT的工作原理。

至于类型类,它们与面向对象的类没有任何关系。 如果你眯着眼睛,它们看起来有点像C#界面,但它们不是! 例如,类型类可以提供默认实现。 类型类解析也是纯静态的; 它与运行时调度无关,因为要调用的函数都是在编译时确定的。 有时,将使用的类型类的实例取决于函数调用的返回类型 ,而不是任何参数。 你最好不要把它翻译成OO术语,因为它们不是一回事。

GHC的类型类实现实际上是通过创建一个字典来实现的,该字典作为隐式参数传递给在其签名中具有类型类约束的函数。 即,如果类型看起来像Num a => a -> a -> a ,编译器将使用Num特定函数的字典传递一个额外参数,该函数用于在该调用站点处使用的实际类型。 因此,如果使用Int参数调用该函数,它将使用来自NumInt实例的函数获得额外的字典参数。

本质上,签名是“只要您可以为要使用的类型提供Num类型类中的操作,此函数是多态的”,编译器确实将它们作为函数的额外参数提供。

话虽这么说,GHC有时能够完全优化掉整个额外的字典参数,并且只是内联必要的function。