3

我对 Haskell 编程很陌生。我正在尝试处理它的类、数据、实例和新类型。这是我的理解:

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<T> {
  public bool method1(List<T> someParam);
 }

// or

abstract class SomeClass<T> {
  public abstract bool method1(List<T> someParam);
}

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

为了

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

这些都是正确的吗?newtype 怎么样,我如何用 C# 或 Java 来表示它?

4

4 回答 4

5

正如其他人所说,它更像是一个有区别的联合——这是一个只有 C/C++ 程序员可能听说过的晦涩的结构。

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

data Stuff = Small Int | Big String Double Bool

现在,如果我写Small 5,这是一个Stuff只有 1 个字段的值。(它占用了这么多 RAM。)但如果我这样做Big "Foo" 7.3 True,这也是type 的值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 中,我们通常不用担心 getter 和 setter。简单地定义一个类型就足以创建该类型的值并访问它们的内容。它有点像一个 C# struct,所有字段都标记为public readonly. (当我们确实担心 getter 时,我们通常称它们为“投影函数”......)

在 OO 中,您使用类进行封装。在 Haskell 中,您可以使用modules来做到这一点。在模块内部,一切都可以访问一切(就像类可以访问自身的每个部分一样)。您使用导出列表来说明模块的哪些部分对外界公开。特别是,您可以公开类型名称,同时完全隐藏其内部结构。那么创建或操作该类型值的唯一方法是您从模块中公开的函数。


你问的newtype

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

newtype UserID = ID Int

nowUserID是一种全新的类型,与任何事物完全无关。但在幕后,它实际上只是好老的另一个名字Int。这意味着您不能UserID在需要 a 的地方Int使用 - 也不能在需要IntaUserID的地方使用。因此,您不能仅仅因为它们都是整数而将用户 ID 与其他一些随机数混淆。

你可以做同样的事情data

data UserID = ID Int

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

(轻微的挑剔:实际上要做到相同,您需要说

data UserID = ID !Int

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

于 2013-08-25T09:56:37.487 回答
4

使 sum 类型像

data NewData = Constr1 Int Int | Constr2 String Float

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

interface INewDataVisitor<out R> {
    R Constr1(Constr1 constructor);
    R Constr2(Constr2 constructor);
}

interface INewData {
    R Accept<R>(INewDataVisitor<R> 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<R>(INewDataVisitor<R> 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<R>(INewDataVisitor<R> visitor) {
        return visitor.Constr2(this);
    }
}

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

一个 c# 接口,如

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

在 Haskell 中实际上更像是以下内容:

data SomeInterface t = SomeInterface {
    method1 :: [t] -> bool
}
于 2013-08-25T07:03:57.703 回答
4

考虑 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 a b) = a + b > 0
foo (ConStr2 a b) = 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
}

为了完整起见,这里是ConStr1使用上述 C 定义的构造函数的实现:

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# 代码,它不担心浪费空间,只是将 astruct用于联合。这是一篇MSDN 文章,讨论了如何获得 C 样式联合的重叠效果。

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

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

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

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

于 2013-08-25T07:30:59.743 回答
2

Haskell 的数据类型与任何特定的 C# 构造都不完全相同。您可以期望的最好的结果是获得一些功能的模拟。最好按照自己的方式理解 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使用的实际类型的特定函数的字典。a因此,如果使用Int参数调用该函数,它将获得一个额外的字典参数,其中包含Int来自Num.

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

话虽如此,GHC 有时能够完全优化掉整个额外的字典参数,而只是内联必要的函数。

于 2013-08-26T03:48:07.477 回答