21

根据我在 StackOverflow 上找到的一些建议,我正在研究 Haskell。我很高兴看到 Haskell 的参数化类型的行为与 C# 泛型非常相似。两种语言都建议使用单个字母作为类型参数(通常),并且两种语言似乎都遵循类似的过程来用实际类型替换类型参数。因此,我很快就理解了这个概念。

这导致了这一点:Haskell 的参数化类型与 C# 泛型类型有哪些不同?我从学习 Ruby 中知道,如果你认为从一种语言熟悉的概念在你刚接触的另一种语言中是相同的,你可能会遇到很大的麻烦。通常,当功能实际上非常相似时,问题会更糟……因为它们通常不是100% 相同的。那么,如果我假设我根据我对 C# 泛型的了解理解了参数化类型,那么我可能会被哪些“陷阱”所困扰?

谢谢。

4

4 回答 4

31

这是要记住的一个区别:

C# 有子类型,但 Haskell 没有,这意味着,一方面,您只需查看 Haskell 类型即可了解更多内容。

id :: a -> a

这个 Haskell 函数接受一个类型的值并返回相同类型的相同值。如果你给它 a Bool,它会返回 a Bool。给它一个Int,它会返回一个Int。给它一个Person,它会返回一个Person

在 C# 中,你不能这么确定。这就是 C# 中的“函数”:

public T Id<T>(T x);

现在,由于子类型化,您可以这样称呼它:

var pers = Id<Person>(new Student());

虽然pers是 type Person,但Id函数的参数不是。事实上pers,它可能有一个更具体的类型,而不仅仅是Person. Person甚至可以是抽象类型,保证pers将具有更具体的类型。

如您所见,即使使用像id.NET 类型系统这样简单的功能,也已经比 .NET 中更严格的类型系统提供了更多的功能Haskell。虽然这对于做一些编程工作可能很有用,但它也使得仅仅通过查看事物的类型来推理程序变得更加困难(这在 Haskell 中很有趣)。


第二件事是,Haskell 中通过一种称为“类型类”的机制存在临时多态性(也称为重载)。

equals :: Eq a => a -> a -> Bool

此函数检查两个值是否相等。但不仅仅是任何两个值,只是具有Eq类实例的值。这有点像 C# 中类型参数的约束:

public bool Equals<T>(T x, T y) where T : IComparable

但是,有区别。一方面,子类型化:你可以用它实例化它并用它Person调用它。StudentTeacher

但是这也有不同之处。C# 代码几乎完全按照它的类型编译。类型检查器确保参数实现正确的接口,并且比你好。

而 Haskell 代码符合如下内容:

equals :: EqDict -> a -> a -> Bool

该函数有一个额外的参数,一个包含所有它需要做的Eq事情的函数的字典。以下是如何使用这个函数,以及它编译成的内容:

b1 = equals 2 4          --> b1 = equals intEqFunctions 2 4
b2 = equals True False   --> b2 = equals boolEqFunctions True False

这也显示了是什么让子类型化如此痛苦,如果可能的话,想象一下。

b3 = equals someStudent someTeacher
     --> b3 = equals personEqFunctions someStudent someTeacher

字典应该如何personEqFunctions确定 aStudent是否等于 a Teacher?他们甚至没有相同的字段。

简而言之,虽然 Haskell 类型约束乍一看可能看起来像 .NET 类型约束,但它们的实现方式完全不同,并且编译成两个完全不同的东西。

于 2009-04-06T08:17:09.810 回答
18

我们现在也可以用 Haskell 类型的类做其他事情。在 Haskell 中搜索“泛型”开辟了更高级别的多态泛型编程的整个领域,超出了大多数人认为的“泛型”的标准参数多态。

例如,GHC 最近获得了类型族,支持各种有趣的类型编程功能。一个非常简单的例子是任意多态容器的每种类型的数据表示决策。

我可以为说,列表,

class Listy a where

    data List a 
             -- this allows me to write a specific representation type for every particular 'a' I might store!

    empty   :: List a
    cons    :: a -> List a -> List a
    head    :: List a -> a
    tail    :: List a -> List a

我可以编写对任何实例化 List 进行操作的函数:

map :: (Listy a, Listy b) => (a -> b) -> List a -> List b
map f as = go as
  where
    go xs
        | null xs   = empty
        | otherwise = f (head xs) `cons` go (tail xs)

然而,我们从未给出特定的表示类型。

现在这是一个通用列表的类。我可以根据元素类型给出特别巧妙的表示。因此,例如对于 Int 列表,我可能会使用一个数组:

instance Listy Int where

data List Int = UArray Int Int

...

所以你可以开始做一些非常强大的泛型编程。

于 2009-04-07T06:01:19.800 回答
7

另一个很大的区别是 C# 泛型不允许对类型构造函数(即 * 以外的种类)进行抽象,而 Haskell 允许。尝试将以下数据类型转换为 C# 类:

newtype Fix f = In { out :: f (Fix f) }
于 2009-06-16T11:23:00.170 回答
6

为了跟进,“您可能会遇到大麻烦,认为您从一种语言熟悉的概念在另一种语言中是相同的[您是新手]”这个问题的一部分:

这是您在使用 Haskell 类型类时需要了解的关键区别(例如,Ruby)。给定一个函数,例如

add :: Num a => a -> a -> a
add x y = x + y

这并不意味着x都是y任何类型的类Num。这意味着xy是完全相同的类型,哪个类型属于 class Num。“嗯,你当然会说;a 和 a一回事。” 我也是这么说的,但是我花了好几个月才停止认为如果x是一个Int并且y是一个Integer,这就像在 Ruby 中添加一个Fixnum和一样。Bignum相当:

*Main> 添加 (2::Int) (3::Integer)

<交互式>:1:14:
    无法将预期类型“Int”与推断类型“Integer”匹配
    在`add'的第二个参数中,即`(3 :: Integer)'
    在表达式中:add (2 :: Int) (3 :: Integer)
    在 `it' 的定义中:it = add (2 :: Int) (3 :: Integer)

换句话说,子类化(尽管这两个Num实例当然也是 的实例Eq)和鸭子类型已经消失了,宝贝。

这听起来很简单明了,但需要相当长的时间来训练自己本能地理解这一点,而不仅仅是智力上的理解,至少如果您来自 Java 和 Ruby 多年。

不,一旦我掌握了这个窍门,我就不会错过子类化了。(嗯,也许偶尔会有点,但我得到的比失去的多得多。当我真的想念它时,我可以尝试滥用存在类型。)

于 2009-06-15T15:02:51.907 回答