Haskell中的返回类型多态性

4

我想要理解Haskell中的多态性。给定下面的典型示例:

module Main where

data Dog = Dog
data Cat = Cat

class Animal a where
    speak :: a -> String
    getA :: a

instance Animal Dog where
    speak _ = "Woof"
    getA = Dog

instance Animal Cat where
    speak _ = "Meow"
    getA = Cat

doA animal = do
    putStrLn $ speak animal

main :: IO ()
main = do
    doA Dog
    doA Cat
    doA (getA :: Dog)

我有一个getA函数,它是Animal类型类的一部分,它按预期工作。只要我提供像read这样的类型注释,我就可以使用getA

然而,当我尝试定义一个独立的函数,如下所示,它无法编译通过。为什么会出现错误?

getA' :: Animal a => a
getA' = if True then Dog else Cat

为什么独立函数 getA' 无法工作,而 getA 可以?

1
doAdo 的无用使用。不如尝试 putAnimal animal = putStrLn $ show animal - Micha Wiedenmann
1个回答

9
这是一个非常常见的错误:忽略了多态的方向。
简单来说:是函数的调用者选择类型参数,而不是函数的实现者。
稍微详细一点:当您给函数签名添加类似于“Animal a => a”的内容时,您向任何调用您的函数的调用者做出了承诺,该承诺读作:“选择一种类型。任何类型。无论您想要什么类型。让我们称其为'a'。现在确保有一个实例'Animal a'。现在我可以返回类型为'a'的值。”
因此,当您编写这样的函数时,您不能返回您选择的特定类型。您必须返回调用您的函数时调用者将选择的任何类型。
为了通过具体示例来加深理解,请想象您的“getA'”函数是可能的,然后考虑以下代码:
data Giraffe = Giraffe

instance Animal Giraffe where
  speak _ = "Huh?"
  getA = Giraffe

myGiraffe :: Giraffe
myGiraffe = getA'  -- does this work? how?

使用类型类方法可以实现这一点,因为调用者所调用的不是同一个函数。这是两个不同的函数,一个是针对Dog,另一个是针对Cat,只是恰好共享相同的名称。

当调用者开始调用其中一个函数时,他们需要以某种方式选择其中一个。有两种方法可以做到这一点:要么(1)他们知道他们想要的确切类型,然后编译器可以查找相应类型的函数,要么(2)其他人以某种方式将Animal实例传递给他们,而正是该实例包含对函数的引用。


现在,如果你真正想要创建一个系统,其中只能有有限数量的动物(即仅有CatDog),并且getA'函数将根据原因返回其中之一,则你需要的不是类型类,而只是像这样的ADT:
data Animal = Cat | Dog

speak :: Animal -> String
speak Cat = "Meow"
speak Dog = "Woof"

getA' :: Animal
getA' = if True then Dog else Cat

在这里,函数getA'将正常工作,因为CatDog都是Animal类型的值。所有类型始终是已知的,没有通用的东西。
问:好的,但是这样,如果我想添加Giraffe,我不能在另一个模块中稍后执行此操作,我必须修改Animal类型。我不能两全其美吗?
简短回答:不行。这是一个众所周知的问题,称为“The Expression Problem”,基本思想是你可以提前知道所有事情(“封闭世界”),或者您可以稍后添加更多内容(“开放世界”),但是您不能同时拥有两者。 显而易见!
但在Haskell中,你还是可以这样做。但实际上不完全是这样的。这有点高级,请忽略如果它看起来很混乱。
你可以添加另一种类型,其中将包含一个动物值加上它的Animal实例。两者都被包裹在盒子里。它看起来像这样:
data SomeAnimal where
  SomeAnimal :: Animal a => a -> SomeAnimal

然后,您可以通过包装 CatDog 来构造此类型的值:

aCat :: SomeAnimal
aCat = SomeAnimal Cat

aDog :: SomeAnimal
aDog = SomeAnimal Dog

请注意,aCataDog都是相同类型的SomeAnimal。这是关键点。它们是不同类型的值包装在外观相同的盒子中,并且该盒子还包含它们各自的Animal实例。
这意味着,如果您打开盒子,您将获得该值及其Animal实例,这反过来意味着您可以使用Animal方法。例如:
someSpeak :: SomeAnimal -> String
someSpeak (SomeAnimal a) = speak a

有了这个,你可以这样实现你的getA'函数:

getA' :: SomeAnimal
getA' = if True then SomeAnimal Dog else SomeAnimal Cat

然而,“表达式问题”仍然存在,因为我其实有点撒谎了:它并不是关于“封闭世界”与“开放世界”的区别,而是关于扩展操作集合与扩展可能值集合的区别。其中一个总是容易的,另一个则很难(详见链接)。这也适用于这个案例:
如果你让 “Cat” 和 “Dog” 成为相同类型的值,你可以轻松地添加更多函数,但如果你想要添加更多动物,你必须找到所有那些已经制作好的函数并修改它们。困难。
如果你使它们成为不同类型,并走“SomeAnimal”的路线来统一它们,你可以轻松地添加更多动物 - 只需创建一个类型并实现“Animal”类即可。但如果你想要添加更多功能,你必须浏览所有那些已经制作好的动物,并为每个“Animal”实例添加新功能的具体实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接