首页 > 解决方案 > 类型类默认方法实例化中的模糊类型解析

问题描述

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

{-# 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
   |                            ^^^

我在这里遗漏了一些明显的东西吗?

标签: haskelltypestype-inferencetypeclass

解决方案


你所有的方法都是模棱两可的。

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

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?编译器不知道。无处可寻。

可是等等!Cmatcha ~ Int和的两个实例b ~ String,那么选择哪个?不清楚。没有足够的信息。模糊的。

这正是您尝试调用getand changein时发生的情况map change . get:没有足够的类型信息让编译器了解调用或调用的a,bc是什么。哦,请记住:这两个调用可能来自不同的实例。没有什么可以说它们必须来自与自身相同的实例。getchangechangeAll


有两种可能的方法来解决这个问题。

首先,您可以使用函数依赖,这是一种说法,为了确定c,知道a和就足够了b

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

如果您以这种方式声明类,编译器将拒绝相同aand的多个实例b,但不同c的是,另一方面,它只需知道aand就可以选择实例b

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

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

请记住,对于您的changeAll函数,即使这三个函数依赖项也不够,因为changeAll"swallows"的实现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

(注意:为了能够引用类型变量a, b, 和c这样的函数体,您还需要启用ScopedTypeVariables

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

foo = changeAll @Int @String @Bool 42

推荐阅读