首页 文章

类型必不可少?

提问于
浏览
32

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

回答这个问题的最简单方法是:使用数据

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

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

在_3044156上回答类似的问题 . 加布里埃尔冈萨雷斯说

如果出现以下情况,请使用类型类:每个给定类型只有一个正确的行为类型类具有所有实例必须满足的关联方程(即“法则”)

嗯..

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

6 回答

  • 2

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

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

    那么,我们来谈谈字典传递 .

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

    像C#这样的语言使您能够破解扩展方法来伪造新东西,而scala等语言中的“traits”和其他语言中的多重继承也允许您委派一些工作,但它们是部分解决方案 .

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

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

    为什么?

    因为我们可以将字典的使用转移到使用站点,而不必随身携带数据类型,我们可以依赖这样一个事实:当我们这样做时,没有任何关于代码行为的改变 .

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

    对于像 Set 这样的简单示例,您可以使用手动字典翻译 . 价格看起来不那么高 . 您必须在字典中烘焙,比如,当您制作对象时如何对 Set 进行排序,然后 insert / lookup ,只会保留您的选择 . 这可能是您可以承担的成本 . 当你结合两个 Set 时,当然,它在空中命令你得到 . 也许你拿小一点然后把它插入更大的,但是顺序会随着时间的推移而改变,所以你必须说,左边并且总是将它插入右边,或记录这种随意的行为 . 你're now being forced into suboptimal performing solutions in the interest of '灵活' .

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

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

    这些是类型类很容易实现的东西 .

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

    不是由一个长镜头 .

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

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

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

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

  • 30

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

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

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

  • 41

    要回答这部分问题:

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

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

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

  • 1

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

  • 5

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

    人们倾向于低估类型类提供的便利性,可能是因为他们从未尝试过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)
    

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

  • 3

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

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

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

    • 如果Haskell是您认真选择的工具 software engineering ,那么类型类很强大且必不可少 .

    • 如果您是初学者使用haskell learn (功能性) programming ,类型的复杂性和难度可以超过优势 - 当然在你的学习开始时 .

    这里有几个例子比较ghc和gofer(拥抱的前身,现代haskell的前身):

    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
    

相关问题