将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
类型的变量只能包含Red
, Green
或Blue
,而这些变量不是任何整数。 如果愿意,可以定义一个函数将它们转换为整数,但这不是编译器存储它们的方式。
你对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
参数调用该函数,它将使用来自Num
的Int
实例的函数获得额外的字典参数。
本质上,签名是“只要您可以为要使用的类型提供Num类型类中的操作,此函数是多态的”,编译器确实将它们作为函数的额外参数提供。
话虽这么说,GHC有时能够完全优化掉整个额外的字典参数,并且只是内联必要的function。