typeclass默认方法实例中的歧义类型解析

问题描述 投票:3回答:1

为什么以下代码无法正确键入?

{-# LANGUAGE AllowAmbiguousTypes, MultiParamTypeClasses #-}

module Main where

class Interface a b c where
  get :: a -> [b]
  change :: b -> c

  changeAll :: a -> [c]
  changeAll = map change . get

main = return ()

如果我注释掉--changeAll = map change . get的默认实例,一切似乎都很好。但是,在实例化到位后,出现此错误:

GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( test.hs, interpreted )

test.hs:10:19: error:
    • Could not deduce (Interface a0 b0 c)
        arising from a use of ‘change’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘a0’, ‘b0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the first argument of ‘map’, namely ‘change’
      In the first argument of ‘(.)’, namely ‘map change’
      In the expression: map change . get
   |
10 |   changeAll = map change . get
   |                   ^^^^^^

test.hs:10:28: error:
    • Could not deduce (Interface a b0 c0) arising from a use of ‘get’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘b0’, ‘c0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the second argument of ‘(.)’, namely ‘get’
      In the expression: map change . get
      In an equation for ‘changeAll’: changeAll = map change . get
   |
10 |   changeAll = map change . get
   |                            ^^^

我在这里错过明显的东西吗?

haskell types type-inference typeclass
1个回答
6
投票

您的所有方法均含糊不清。

为了更好地说明问题,让我们将示例简化为一种方法:

class C a b c where
    get :: a -> [b]

现在假设您有以下实例:

instance C Int String Bool where
    get x = [show x]

instance C Int String Char where
    get x = ["foo"]

然后想象您正在尝试调用该方法:

s :: [String]
s = get (42 :: Int)

根据s的签名,编译器知道b ~ String。通过get的参数,编译器知道a ~ Int。但是什么是c?编译器不知道。无处可收集。

但是等等! C的两个实例都匹配a ~ Intb ~ String,那么该选择哪个呢?不清楚。信息不足。模棱两可。

这就是当您尝试在get中调用changemap change . get时发生的事情:没有足够的类型信息供编译器理解abc用于get呼叫或change呼叫。哦,请记住:这两个调用可能来自不同的实例。没有什么可说的,它们必须与changeAll本身来自同一实例。


有两种方法可以解决此问题。

首先,您可以使用功能依赖项,这是一种表示要确定c的方法,即足以知道ab

class C a b c | a b -> c where ...

如果以这种方式声明该类,则编译器将拒绝具有相同ab,但具有不同c的多个实例,并且在另一方面,只需知道[ C0]和a

当然,您可以在同一个类上具有多个功能依赖项。例如,您可以声明知道任何两个变量就足以确定第三个变量:

b

请记住,对于您的class C a b c | a b -> c, a c -> b, b c -> a where ... 函数,这三个函数依赖项还不够,因为changeAll的实现会“吞咽” changeAll。也就是说,当它调用b时,唯一已知的类型是get。同样,当它调用a时,唯一已知的类型是change。这意味着,为了使c的这种“吞咽”工作,必须单独由b以及单独由a确定:

c

当然,只有在程序逻辑确实具有某些变量由其他变量确定的属性的情况下,才有可能。如果您确实需要所有变量都独立,请继续阅读。


其次,您可以使用class Interface a b c | a -> b, c -> b where ... 明确告诉编译器类型必须是什么

TypeApplications

不再有歧义。编译器确切知道要选择哪个实例,因为您已明确告诉它。

将此应用于您的 s :: String s = get @Int @String @Bool 42 -- works 实现:

changeAll

((注:为了能够像这样在函数体中引用类型变量changeAll :: a -> [c] changeAll = map (change @a @b @c) . get @a @b @c ab,还需要启用c

当然,在调用ScopedTypeVariables本身时,您也需要这样做,因为它的类型签名中也没有足够的信息:

changeAll
© www.soinside.com 2019 - 2024. All rights reserved.