【问题标题】:In Haskell, why are types ambiguous even with AllowAmbiguousTypes enabled?在 Haskell 中,为什么即使启用了 AllowAmbiguousTypes,类型也会模棱两可?
【发布时间】:2019-10-20 09:49:08
【问题描述】:

假设你有两个这样定义的类型类:

{-# LANGUAGE MultiParamTypeClasses #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

然后你想通过使用 f 和 g 以一般方式定义一个新函数 h。

h a = g (f a)

我们知道这个函数的类型是a -> b,所以c 是隐含的。我想把它留给fg 的实现者c 可能是什么。 Haskell 抱怨这句话 c 含糊不清。

然后按照错误的建议,我打开了这个扩展:

{-# LANGUAGE AllowAmbiguousTypes #-}

现在可以了!不错。

我相信通常作为一种良好的软件工程实践,我想为我的函数编写明确的规范,以告诉编译器我期望我的函数应该如何表现。这样以后的编译器就可以抱怨我不尊重我的设置。

所以我想在它之前添加我的函数的类型:

h :: (F a c, G c b) => a -> b
h a = g (f a)

现在类型歧义错误又来了……为什么?

总结一下为什么 Haskell 会抱怨下面这段代码?即使 AllowAmbiguousTypes 被明确启用。如何在保持明确的函数类型定义的同时修复它?我知道删除函数的类型定义可以解决问题,但我不想指定不足。

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h :: (F a c, G c b) => a -> b
h a = g (f a)

为什么没有 Haskell 抱怨这个?

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h a = g (f a)

错误信息:

error:
    * Could not deduce (G c0 b) arising from a use of `g'
      from the context: (F a c, G c b)
        bound by the type signature for:
                   h :: forall a c b. (F a c, G c b) => a -> b

      The type variable `c0' is ambiguous
      Relevant bindings include
        h :: a -> b 
    * In the expression: g (f a)
      In an equation for `h': h a = g (f a)
    |
    | h a = g (f a)
    |       ^^^^^^^
error:
    * Could not deduce (F a c0) arising from a use of `f'
      from the context: (F a c, G c b)
        bound by the type signature for:
                   h :: forall a c b. (F a c, G c b) => a -> b
      The type variable `c0' is ambiguous
      Relevant bindings include
        a :: a 
        h :: a -> b 
    * In the first argument of `g', namely `(f a)'
      In the expression: g (f a)
      In an equation for `h': h a = g (f a)
    |
    | h a = g (f a)
    |          ^^^

【问题讨论】:

  • 因为无法根据类型签名确定c 应该是什么。所以函数的“用户”,不能决定c
  • @WillemVanOnsem 但如果您不指定 h :: (F a c, G c b) => a -> b,它会起作用。那么为什么它在一种情况下有效而在另一种情况下无效呢?
  • 我不是这些问题的专家,但我认为问题在于 GHC 不知道您要使用哪个 c,启用功能依赖可能会有所帮助。
  • 如果启用-XScopedTypeVariables并将签名更改为h :: forall c b a. (F a c, G c b) => a -> b(实现中可以引用中间类型c),同时启用-XTypeApplications,则可以写h a = g @c (f a)

标签: haskell typeclass


【解决方案1】:

您的代码不明确,编译器无法自动解决。假设:

class F a c where f :: a -> c
class G c b where g :: c -> b

instance F Int String where f = show
instance G String Bool where g = null

h :: (F Int c, G c Bool) => Int -> Bool
h a = g (f a)

现在,最后一行使用了哪些实例?我们有两个选择:使用上下文(F Int c, G c Bool) 提供的实例,或者忽略该上下文并使用上面的实例,String 作为中间类型。两种解释都是正确的,确实可以明确写出

h1 :: forall c. (F Int c, G c Bool) => Int -> Bool
h1 a = (g :: c -> Bool) (f a)

h2 :: forall c. (F Int c, G c Bool) => Int -> Bool
h2 a = (g :: String -> Bool) (f a)

选择一种方式或另一种方式。 GHC无法以合理的方式为我们做这个选择。它可以根据一些启发式方法选择一个,但这可能会让程序员大吃一惊。因此,我们可以争辩说,GHC 绝对不能选择,报告歧义,并让程序员澄清他们的意图。

最后,请注意,您的代码不包含上述两个实例这一事实是无关紧要的,因为这些实例可以稍后添加,甚至可以添加到另一个模块中,因此 GHC 必须保守,避免假设它们永远不会存在。

为什么 Haskell 不抱怨这个?

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h x = g (f x)  -- [renamed to x for clarity]

好点子。这里GHC可以找到一个最通用的类​​型,就是

h :: forall a b c. (F a c, G c b) => a -> b
h x = (g :: c -> b) ((f :: a -> b) x)

由于这里是GHC添加了类型变量c,所以GHC可以确定该类型是中间类型。毕竟,该类型变量是在类型推断期间创建的,以表示中间类型。

相反,当用户显式写入上下文时,GHC 无法猜测用户的意图。即使在实践中不太可能,用户也有可能不想使用该实例,而是使用另一个实例(在程序中可用,但在上下文中不存在)。

考虑一下这个案例可能会有所帮助:

data T = ....

h :: forall a b c. (F a c, G c b, F a T, G T b) => a -> b
h x = g (f x)

我想你可以同意这段代码应该被拒绝:中间类型可能是Tc,没有理智的方法可以解决它。现在考虑这种情况:

h :: forall a b c. (F a c, G c b) => a -> b
h x = g (f x)

instance F a T where ...
instance G T b where ...

现在,这与之前的情况并没有太大的不同。我们没有在上下文中有两个选项,而是将一个移到了外面。不过,GHC 仍有两个选项可供选择。所以,再一次,理智的事情是拒绝代码,并要求程序员提供更多细节。


一个更简单的场景,在 GHCi 中:

> :set -XScopedTypeVariables
> :set -XAllowAmbiguousTypes
> class C a where c :: String
> instance C Int where c = "Int"
> instance C Bool where c = "Bool"
> let g :: forall a. C a => String ; g = c

<interactive>:7:40: error:
    * Could not deduce (C a0) arising from a use of `c'

在这里,GHC 怎么会知道,当我写 g = c 时,我的意思是“c 来自上下文 C a?我可以写成这个意思是“c 来自 Int 的实例” . 或Bool.

GHC 在内部生成一个新的类型变量a0,然后尝试解决约束C a0。它有三个选择:选择a0 = aa0 = Inta0 = Bool。 (以后可能会添加更多实例!)

所以,它是模棱两可的,如果不猜测程序员的意图,就没有理智的方法来解决它。唯一安全的选择是拒绝。

【讨论】:

  • 我不确定你的回答。我也在学习haskell。为什么定义 h 函数在我只写h a = g (f a) 而不指定其类型时起作用。等你刚刚编辑。
  • @J.M.我最初错过了这一点。我现在正在写那部分:)
  • 好的,我想我明白了,那么我的问题是如何以一种好的方式实现我的目的?我曾想过使用参数类型,比如让 c 成为 a 的参数,但后来变得一团糟,特别是如果我在一个类型中有多个参数,我不知道如何选择性地重新排序它们或删除它们中的一些..
  • @J.M.为了消除代码歧义,我可以看到的选项是:1)强制用户注释中间类型(使用类型注释或 TypeApplications),2)使用像 class F a c | a -&gt; c 这样的功能依赖项,3)使用类型族和将c 替换为Fun a。 2 和 3 只能在您的实例实际尊重fundep 的情况下使用。一般来说,说服类型推理机器播放想要的曲调并非易事。
猜你喜欢
  • 1970-01-01
  • 2012-12-18
  • 2018-06-01
  • 2015-07-22
  • 1970-01-01
  • 2012-10-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多