Return haskell 中的类型多态性

Return type polymorphism in haskell

我正在尝试了解 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 起作用?

这是一个很常见的错误:忽略了多态的方向。

简而言之:是函数的调用者选择类型参数,而不是实现者 的函数。

稍微长一些: 当你给你的函数一个像 Animal a => a 这样的签名时,你就是在向你的函数的任何调用者做出承诺,并且这个承诺读取了一些东西像这样:“选择一个类型。任何类型。无论你想要什么类型。我们称之为 a。现在确保有一个实例 Animal a。现在我可以 return 你的值类型为 a"

所以你看,当你写这样的函数时,你不会return你选择的特定类型。您必须 return 您函数的调用者稍后调用时选择的任何类型。

用一个具体的例子来说明问题,想象一下你的 getA' 函数是可能的,然后考虑这段代码:

data Giraffe = Giraffe

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

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

使用 class 类型的方法是可行的,因为它与调用者调用的函数不同。这是两个不同的函数,一个用于 Dog,另一个用于 Cat,恰好共享相同的名称。

当调用者开始调用这些函数之一时,他们需要以某种方式选择哪一个。这可以通过两种方式完成:(1)他们知道他们想要的确切类型,然后编译器可以查找该类型的相应函数,或者(2)其他人以某种方式传递了一个 Animal 实例对他们来说,就是那个包含对函数的引用的实例。


现在,如果你真正想做的是创建一个系统,其中可以有数量有限的动物(即只有 CatDog),并且 getA' 函数会 return 其中之一,具体取决于原因,那么您要查找的不是类型 class,而只是一个 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 的值。所有类型总是已知的,没有什么是通用的。

Q: 好的,但是这样的话,如果我要加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

然而,你仍然得到“表达式问题”,因为我实际上撒了一点谎:这不是关于“封闭世界”与“开放世界”的问题,而是关于扩展操作集与扩展操作集的问题可能的值。一个总是容易的,另一个总是困难的(阅读 link 了解详情)。

这也适用于这种情况:

  • 如果你使 CatDog 值相同类型,你可以轻松添加更多功能,但如果你想添加更多动物,你必须找到所有这些功能你已经制作并修改了它们。很难。
  • 如果你让它们成为不同的类型并走 SomeAnimal 路线来统一它们,你就可以轻松添加更多动物 - 只需创建一个类型并实现 Animal class。但是,如果您想添加更多功能,则必须检查您已经制作的所有这些动物,并将新功能的实现添加到它们的每个 Animal 个实例中。