类型必不可少?

时间:2014-09-15 19:28:45

标签: haskell

我曾经在haskell beginners上提出过一个问题,无论是使用data / newtype还是类型类。在我的特定情况下,事实证明不需要类型类。此外,汤姆埃利斯给了我一个很好的建议,如果有疑问该怎么办:

  

回答这个问题的最简单方法是:

     

使用数据

我知道类型类可以使一些东西更漂亮,但AFIK并不多。同样让我感到震惊的是,类型类主要用于脑干,更新的东西,几乎没有引入新的类型类,一切都是用data / newtype完成的。

现在我想知道是否存在绝对需要类型类的情况,并且无法用data / newtype表示事物?

StackOverflow Gabriel Gonzales上回答类似的问题说

  

在以下情况下使用类型类:

     

每种给定类型只有一个正确的行为

     

类型类具有所有实例必须满足的关联方程式(即“法则”)

嗯..

或者类型和数据/新类型有些竞争的概念因历史原因而共存?

6 个答案:

答案 0 :(得分:41)

我认为类型类 是Haskell的重要组成部分。

它们是Haskell的一部分,使其成为我所知道的最简单的重构语言,并且它们是您能够推断代码正确性的重要资产。

所以,让我们谈谈字典传递。

现在,任何类型的字典传递都是传统面向对象语言状态的重大改进。我们知道如何在C ++中使用vtable进行OOP。然而,vtable是对象的一部分'用OOP语言。将vtable与对象融合会强制您将代码转换为一种形式,在这种形式中,您可以使用新功能扩展核心类型的严格规则,它实际上只是该类的原始作者必须将其他人想要烘焙的所有内容合并到一起他们的类型。这导致了"熔岩流代码"和各种其他设计反模式等。

像C#这样的语言让你能够破解扩展方法来伪造新东西,以及" traits"在scala这样的语言和其他语言的多继承中,你也可以委托一些工作,但它们是部分解决方案。

当你从他们操纵的物体中分离出vtable时,你会获得令人兴奋的力量。您现在可以将它们传递到任何您想要的地方,但是当然您需要为它们命名并谈论它们。模块/仿函数的ML规则和显式字典传递方式采用这种方法。

类型角色略有不同。我们依赖于给定类型的类型类实例的唯一性,并且在很大程度上,这个选择允许我们摆脱这种简单的核心数据类型。

为什么?

因为我们可以将字典的使用转移到使用网站,并且不必使用数据类型来携带它们,我们可以依赖于这样的事实:当我们这样做时,没有任何改变关于代码的行为

将代码机械转换为更复杂的手动传递的字典会丢失给定类型的此类字典的唯一性。将程序中的字典传递给程序中的不同点现在会导致程序具有极大的不同行为。您可能需要或可能不必记住您的数据类型构造的字典,如果您希望根据您的参数获得条件行为,那么您就会感到沮丧。

对于像Set这样的简单示例,您可以使用手动字典翻译。价格似乎不高。您必须在字典中烘焙,比如,在制作对象时如何对Set进行排序,然后insert / lookup,只会保留您的选择。这可能是您可以承担的成本。当你现在结合两个Set时,当然,它是在空中命令你得到的。也许你拿小一点然后把它插入更大的,但是顺序会随着时间的推移而改变,所以你必须说,左边并且总是将它插入右边,或记录这种随意的行为。为了“灵活性”,您现在被迫陷入次优的绩效解决方案。

但是Set是一个微不足道的例子。在那里你可以为你正在使用哪个实例的类型加一个索引,只涉及一个类。当你想要更复杂的行为时会发生什么?我们用Haskell做的一件事就是使用monad变换器。现在你有很多的实例 - 而且你没有一个好地方可以存储它们,MonadReaderMonadWriterMonadState等等,可能都是有条件的,基于潜在的monad。当你提升并换掉它时会发生什么,现在不同的东西可能适用也可能不适用?

为此做一个明确的词典是很多工作,没有一个存储它们的好地方,你要求用户采用全局程序转换来采用这种做法。

这些是类型类很容易做到的事情。

我相信你应该将它们用于一切吗?

不是一蹴而就。

但我不同意其他答复,他们对Haskell不重要。

Haskell是唯一提供它们的语言,它们至少对于用这种语言思考的能力至关重要,并且是我考虑Haskell回家的重要原因。

我同意这里的一些事情,在有法律和选择明确的时候使用类型类。

但是,我挑战,如果你没有法律或选择不明确,你可能对如何建立问题域的建模知之甚少,应该寻求一些东西您可以将其纳入类型类模具,甚至可能适用于现有的抽象 - 当您最终找到该解决方案时,您会发现您可以轻松地重复使用它。

答案 1 :(得分:30)

在大多数情况下,类型是不必要的。任何类型类代码都可以机械地转换为dictionary-passing样式。它们主要提供便利,有时是方便的(参见kmett的答案)。

有时,类型类的单实例属性用于强制不变量。例如,您无法安全地将Data.Set转换为字典传递样式,因为如果您使用两个不同的Ord字典插入两次,则可以使数据结构不变。当然,您仍然可以将任何工作代码转换为字典传递样式的工作代码,但是您无法将违反的代码视为非法。

法律是类词组的另一个重要文化方面。编译器不强制执行法律,但Haskell程序员期望类型类带来所有实例都满足的法则。这可以leveraged来提供对某些功能的更强保证。这种优势仅来自社区的惯例,而不是语言的正式属性。

答案 2 :(得分:5)

回答问题的这一部分:

"类型和数据/ newtype有点竞争的概念"

没有。类型类是系统类型的扩展,它允许您对多态参数进行约束。就像编程中的大多数东西一样,它们当然是语法糖[因此它们在其使用不能被其他任何东西取代的意义上并不重要]。这并不意味着他们是多余的。这只是意味着你可以使用其他语言设施来表达类似的事情,但是当你处理它时,你会失去一些清晰度。字典传递可以用于大多数相同的事情,但它在类型系统中最终不那么严格,因为它允许在运行时改变行为(这也是你使用字典传递而不是字典传递的一个很好的例子类型)。

无论你是否有类型类,数据和newtype仍然意味着完全相同:在data作为新类型的数据结构的情况下引入新类型,并且在newtype的情况下引入type的类型安全变体。

答案 3 :(得分:3)

要稍微扩展我的评论,我建议总是先使用数据和字典传递。如果样板和手动实例管道变得太难以承受,那么考虑引入一个类型类。我怀疑这种方法通常会带来更清洁的设计。

答案 4 :(得分:2)

我只想对语法提出一个非常平凡的观点。

人们倾向于低估类型类提供的便利性,可能是因为他们从未尝试过Haskell而不使用任何类型。这是一种“篱笆另一边的草更绿”的现象。

while :: Monad m -> m Bool -> m a -> m ()
while m p body = (>>=) m p $ \x ->
                 if x
                 then (>>) m body (while m p body)
                 else return m ()

average :: Floating a -> a -> a -> a -> a
average f a b c = (/) f ((+) (floatingToNum f) a ((+) (floatingToNum f) b c))
                        (fromInteger (floatingToNum f) 3)

这是类型类的历史动机,它今天仍然有效。如果我们没有类型类,我们肯定需要某种替代它,以避免编写像这样的怪物。 (也许像记录双关语或阿格达的“开放”。)

答案 5 :(得分:1)

  

我知道类型类可以使一些东西更漂亮,但AFIK并不多。

比较漂亮?没有!方式更漂亮! (正如其他人已经注意到的那样)

然而,这个问题的答案在很大程度上取决于这个问题的来源。

  • 如果Haskell是您严肃的软件工程的首选工具,那么类型类就是 强大而必不可少。
  • 如果您是初学者使用haskell来学习(功能性)编程,那么类型的复杂性和难度可能超过优势 - 当然在学习开始时。

以下是将ghc与gofer(拥抱的前身, 现代哈斯克尔的前身:

GOFER

? 1 ++ [2,3,4]
ERROR: Type error in application
*** expression     :: 1 ++ [2,3,4]
*** term           :: 1
*** type           :: Int
*** does not match :: [Int]

现在与ghc比较:

Prelude> 1 ++ [2,3,4]
:2:1:
    No instance for (Num [a0]) arising from the literal `1'
    Possible fix: add an instance declaration for (Num [a0])
    In the first argument of `(++)', namely `1'
    In the expression: 1 ++ [2, 3, 4]
    In an equation for `it': it = 1 ++ [2, 3, 4]

:2:7:
    No instance for (Num a0) arising from the literal `2'
    The type variable `a0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 2
    In the second argument of `(++)', namely `[2, 3, 4]'
    In the expression: 1 ++ [2, 3, 4] 

这应该表明错误信息,不仅是类型不合适,它们可能更加丑陋!

一个人可以一直走(gofer)并使用使用的'简单前奏' 根本没有类型。这使得认真编程非常不现实 但是在Hindley-Milner周围缠绕你的脑袋真的很整洁:

标准序曲

? :t (==)
(==) :: Eq a => a -> a -> Bool
? :t (+)
(+) :: Num a => a -> a -> a

简单序曲

? :t (==)
(==) :: a -> a -> Bool
? :t (+)
(+) :: Int -> Int -> Int