如果我写
foo :: (Num a) => a
foo = 42
GHC乐意接受它,但是如果我写的话
bar :: (Num a) => a
bar = (42 :: Int)
它告诉我预期的类型a
与推断的类型Int
不匹配。我不太明白为什么,因为Int
是Num
所代表的类a
的一个实例。
在尝试编写一个功能时,我遇到了同样的情况,这个功能归结为问题的核心,看起来大致如下:
-- Note, Frob is an instance of class Frobbable
getFrobbable :: (Frobbable a) => Frob -> a
getFrobbable x = x
是否可以编写这样的函数?如何使结果与类型签名兼容?
答案 0 :(得分:11)
您正在将类型类约束视为子类型约束。这是常见的事情,但实际上它们只与逆变情况重合,也就是说,当涉及函数参数而不是结果时。签名:
bar :: (Num a) => a
表示调用者可以选择类型a
,前提是它是Num
的实例。所以在这里,来电者可以选择拨打bar :: Int
或bar :: 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)
会导致以下结果:
someFrob
必须是Frob
类型,因为它是getFrobbable
。(getFrobbable someFrob)
必须属于Frob2
类型,因为它已提供给useFrobbable
getFrobbable someFrob = someFrob
,我们知道getFrobbable someFrob
和someFrob
具有相同的类型。因此,系统得出结论:Frob
和Frob2
是相同的类型,即使它们不是。因此,这种推理是不合理的,这最终是你发布的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