【问题标题】:Writing a function with a universally-quantified return type编写具有通用量化返回类型的函数
【发布时间】:2010-07-22 07:33:08
【问题描述】:

如果我写

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

GHC 欣然接受,但如果我写了

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

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

我在尝试编写一个函数时遇到了同样的情况,归结为问题的核心,大致如下:

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

有可能写出这样的函数吗?如何使结果与类型签名兼容?

【问题讨论】:

    标签: haskell types polymorphism typeclass


    【解决方案1】:

    您将类型类约束视为子类型约束。这是很常见的事情,但实际上它们只在 逆变 情况下重合,即涉及函数 arguments 而不是 results .签名:

    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

    【讨论】:

    • 我无法将getFrobbable 的签名更改为返回Frob 的原因是,在我的真实代码中,该函数是另一个类的一部分——我们称之为FrobGettable。目的是FrobGettable 的不同实例可以定义getFrobbable 以返回Frobbable 的不同实例。不过,这听起来不是一个好的设计。
    • 我会大胆猜测class Frobbable a 的所有方法都是以下形式:foo :: ... -> a -> ...,其中 ...s 不包含a。如果是这种情况(通常是这种模式),则将类 Frobbable 替换为简单的数据类型:data Frobbable = Frobbable { foo :: ..., bar :: ... },其中...s 是对应方法的签名,@987654350 @ 参数已删除。然后让你的类方法返回一个普通的Frobbable。这是“一个错误”链接中描述的技术。
    【解决方案2】:

    一种获得样本效果的方法

    我首先想描述一种方法来实现您的目标。让我们再次看一下您的上一个代码示例:

    -- 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] 列表肯定可以。

    为什么您发布的代码不允许

    现在,为什么不能按原样编写铸造操作?如果您原来的 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 具有相同的类型。

    因此,系统得出结论,FrobFrob2 是同一类型,即使它们不是。因此,这种推理是不合理的,这最终是为什么您发布的getFrobbable 版本没有进行类型检查的原因。

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

        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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-12-28
          • 2018-11-11
          • 1970-01-01
          • 2012-08-18
          • 1970-01-01
          • 2020-05-27
          相关资源
          最近更新 更多