使用通用量化的返回类型编写函数

时间:2010-07-22 07:33:08

标签: haskell types polymorphism typeclass

如果我写

foo :: (Num a) => a
foo = 42

GHC乐意接受它,但是如果我写的话

bar :: (Num a) => a
bar = (42 :: Int)

它告诉我预期的类型a与推断的类型Int不匹配。我不太明白为什么,因为IntNum所代表的类a的一个实例。

在尝试编写一个功能时,我遇到了同样的情况,这个功能归结为问题的核心,看起来大致如下:

-- Note, Frob is an instance of class Frobbable
getFrobbable :: (Frobbable a) => Frob -> a
getFrobbable x = x

是否可以编写这样的函数?如何使结果与类型签名兼容?

4 个答案:

答案 0 :(得分:11)

您正在将类型类约束视为子类型约束。这是常见的事情,但实际上它们只与逆变情况重合,也就是说,当涉及函数参数而不是结果时。签名:

bar :: (Num a) => a

表示调用者可以选择类型a,前提是它是Num的实例。所以在这里,来电者可以选择拨打bar :: Intbar :: Double,他们应该都可以。因此bar :: (Num a) => a必须能够构建任何类型的数字,bar不知道选择了哪种特定类型。

逆变情况完全相同,它恰好与OO程序员在这种情况下的直觉相对应。例如

baz :: (Num a) => a -> Bool

意味着调用者再次选择类型a,并再次baz不知道选择了哪种特定类型。

如果您需要让被调用者选择结果类型,只需更改函数的签名以反映此知识。例如:

bar :: Int

或者在getFrobbable案例getFrobbable :: Frob -> Frob中(这使得该功能变得微不足道)。在发生(Frobbable a)约束的任何地方,Frob都会满足它,所以只需说出Frob

这可能看起来很尴尬,但实际上只是信息隐藏发生在函数式编程的不同边界。有一种方法可以隐藏特定的选择,但这种情况并不常见,我认为它在大多数使用它的情况下都是a mistake

答案 1 :(得分:3)

获得样本效果的一种方法

我首先要描述一种方法来实现它的目的。让我们再看一下你的最后一个代码示例:

-- Note, Frob is an instance of class Frobbable
getFrobbable :: (Frobbable a) => Frob -> a
getFrobbable x = x

这实际上是一个施法操作。它需要Frob并且只是忘记了它是什么,仅保留您拥有Frobbable实例的知识。

Haskell中有一个习惯用来完成这个任务。它需要GHC中的ExistentialQuantification扩展。以下是一些示例代码:

{-# LANGUAGE ExistentialQuantification #-}
module Foo where

class Frobbable a where
  getInt :: a -> Int

data Frob = Frob Int

instance Frobbable Frob where
  getInt (Frob i) = i

data FrobbableWrapper = forall a . Frobbable a => FW a

instance Frobbable FrobbableWrapper where
  getInt (FW i) = getInt i

关键部分是FrobbableWrapper数据结构。有了它,您可以编写以下版本的getFrobbable转换函数:

getFrobbable :: Frobbable a => a -> FrobbableWrapper
getFrobbable x = FW x

如果你想拥有一个异构列表,其元素共享一个共同的类型类,即使它们可能不共享一个共同的类型,这个习惯用法也很有用。例如,虽然Frobbable a => [a]不允许您混合使用Frobbable的不同实例,但列表[FrobbableWrapper]肯定会。{/ p>

为什么不允许发布的代码

现在,为什么你不能按原样编写你的施法操作?如果允许您的原始getFrobbable函数进行类型检查,那么可以实现的目标就在于此。

等式getFrobbable x = x确实应该被视为一个等式。 x未以任何方式进行修改;因此,它的类型也不是。这样做的原因如下:

让我们将getFrobbable与另一个对象进行比较。考虑

anonymousFrobbable :: Frobbable a => a
anonymousFrobbable = undefined

(当你想真正推动自己的直觉时,涉及undefined的代码是尴尬行为的重要来源。)

现在假设有人出现并介绍了数据定义和类似

的功能
data Frob2 = Frob2 Int Int

instance Frobbable Frob2 where
  getInt (Frob2 x y) = y

useFrobbable :: Frob2 -> [Int]
useFrobbable fb2 = []

如果我们跳进ghci,我们可以执行以下操作:

*Foo> useFrobbable anonymousFrobbable 
[]

没有问题:anonymousFrobbable的签名意味着“你选择了一个Frobbable实例,我假装我属于那种类型。”

现在,如果我们尝试使用您的getFrobbable版本,则会调用

useFrobbable (getFrobbable someFrob)

会导致以下结果:

  1. someFrob必须是Frob类型,因为它是getFrobbable
  2. (getFrobbable someFrob)必须属于Frob2类型,因为它已提供给useFrobbable
  3. 但是通过等式getFrobbable someFrob = someFrob,我们知道getFrobbable someFrobsomeFrob具有相同的类型。
  4. 因此,系统得出结论:FrobFrob2是相同的类型,即使它们不是。因此,这种推理是不合理的,这最终是你发布的getFrobbable版本没有进行类型检查的理性背后的类型。

答案 2 :(得分:2)

另外值得注意的是,文字42实际上代表fromInteger (42 :: Integer),它实际上是(Num a) => a类型。请参阅Haskell Report on numeric literals

类似的机制适用于浮点数,你可以让GHC使用重载的字符串文字,但对于其他类型(据我所知),没办法这样做。

答案 3 :(得分:0)

我对你要做的事情感到困惑,但是如果你只是想让调用者使用返回值来驱动实例选择,那么这就是正常的类型类工作。

data Frob = Frob Int Float

class FrobGettable a where
    getFrobbable :: Frob -> a

instance FrobGettable Int where
    getFrobbable (Frob x _) = x

instance FrobGettable Float where
    getFrobbable (Frob _ y) = y

instance FrobGettable Char where
    getFrobbable _ = '!'

frob = Frob 10 3.14

test1 :: Float
test1 = getFrobbable frob * 1.1

test2 :: Int
test2 = getFrobbable frob `div` 4

test3 = "Hi" ++ [getFrobbable frob]

我们可以使用GHCi来查看我们的内容,

*Main> :t getFrobbable
getFrobbable :: (FrobGettable a) => Frob -> a