haskell - 类型约束最终变得模棱两可

标签 haskell type-constraints

在我正在使用的Haskell应用程序中,我有一个API,我正在其中尝试设置一组可插入的后端。我将有几种不同的后端类型,我希望调用者(现在只是测试套件)来确定实际的后端。但是,我得到了一个模棱两可的类型错误。

class HasJobQueue ctx queue where
    hasJobQueue :: JobQueue queue => ctx -> queue

class JobQueue q where
    enqueue :: MonadIO m => Command -> q -> m ()

type CloisterM ctx queue exc m = ( Monad m, MonadIO m, MonadError exc m, MonadReader ctx m
                                 , AsCloisterExc exc
                                 , HasJobQueue ctx queue
                                 , JobQueue queue
                                 )

createDocument :: forall ctx queue exc m. CloisterM ctx queue exc m => Path -> Document -> m DocumentAddr
createDocument path document = do
    ...
    queue   <- hasJobQueue <$> ask
    enqueue (SaveDocument addr document) queue
    ...

因此,对我来说,这似乎很清楚。在createDocument中,我想要检索上下文,并从中检索作业队列,调用者将定义该任务并将其附加到上下文。但是Haskell不同意并给了我这个错误:
• Could not deduce (JobQueue q0)
    arising from a use of ‘hasJobQueue’
  from the context: CloisterM ctx queue exc m
    bound by the type signature for:
               createDocument :: CloisterM ctx queue exc m =>
                                 Path -> Document -> m DocumentAddr
    at src/LuminescentDreams/CloisterDB.hs:32:1-105
  The type variable ‘q0’ is ambiguous
• In the first argument of ‘(<$>)’, namely ‘hasJobQueue’

这是我要构建的示例,该示例来自我的API测试套件,其中我使用简单的IORef来模拟所有后端,其中生产将具有其他后端实现
data    MemoryCloister  = MemoryCloister WorkBuffer
newtype WorkBuffer      = WorkBuffer (IORef [WorkItem Command]) 

instance JobQueue WorkBuffer where 
    hasJobQueue (MemoryCloister wb) = wb

instance JobQueue WorkBuffer where
    ... 

因此,我到底需要做些什么来帮助类型检查器理解MonadReader中的上下文包含一个实现JobQueue类的对象?

整个数据类型文件,in this project,包括我最终如何重新构造JobQueue来获得比以上更灵活的内容,

最佳答案

尽管很难根据给定的代码和上下文确切地知道什么是解决问题的正确方法,但您看到的错误是由HasJobQueue类型类引起的,该类非常普遍:

class HasJobQueue ctx queue where
  hasJobQueue :: JobQueue queue => ctx -> queue

从类型检查器的角度来看,hasJobQueuea -> b的函数,外加一些约束(但是约束通常不会影响类型推断)。这意味着,为了调用hasJobQueue,其输入和输出都必须由其他类型信息源完全明确地指定。

如果这令人困惑,请考虑与typechecker几乎完全相同的类:
class Convert a b where
  convert :: a -> b

该类型类通常是一个反模式(正是因为它使类型推断变得非常困难),但是从理论上讲,它可以用于提供在任何两种类型之间进行转换的实例。例如,可以编写以下实例:
instance Convert Integer String where
  convert = show

…然后使用convert将整数转换为字符串:
ghci> convert (42 :: Integer) :: String
"42"

但是,请注意,以下将而不是起作用:
ghci> convert (42 :: Integer)

<interactive>:26:1: error:
    • Ambiguous type variable ‘a0’ arising from a use of ‘print’
      prevents the constraint ‘(Show a0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘a0’ should be.

这里的问题是GHC不知道应该使用什么b,因此无法选择要使用的Convert实例。

在您的代码中,hasJobQueue几乎相同,尽管细节有些复杂。此问题出现在以下几行中:
queue <- hasJobQueue <$> ask
enqueue (SaveDocument addr document) queue

为了知道使用哪个HasJobQueue实例,GHC需要知道queue的类型。幸运的是,GHC可以根据绑定的使用方式推断出绑定的类型,因此希望可以推断出queue的类型。它是enqueue的第二个参数,因此我们可以通过查看enqueue的类型来了解发生了什么:
enqueue :: (JobQueue q, MonadIO m) => Command -> q -> m ()

在这里,我们看到了问题。 enqueue的第二个参数必须具有q类型,该类型也不受限制,因此GHC不会获得任何其他信息。因此,它无法确定q的类型,并且不知道要使用哪个实例来调用hasJobQueue或调用enqueue

那么如何解决呢?嗯,一种方法是为queue选择一种特定的类型,但是根据您的代码,我敢打赌这实际上并不是您想要的。更有可能的是,每种特定的ctx都有特定的队列类型,因此hasJobQueue的返回类型实际上应该由其第一个参数隐含。幸运的是,Haskell有一个概念可以对该事物进行编码,并且该概念是功能依赖项。

还记得我在开始时说过约束通常不会影响类型推断吗?功能依赖性改变了这一点。当您写一个Fundep时,您指出类型检查器实际上可以从约束中获取信息,因为某些类型变量暗示了其他一些变量。在这种情况下,您希望queue隐含ctx,因此您可以更改HasJobQueue的定义:
class HasJobQueue ctx queue | ctx -> queue where
  hasJobQueue :: JobQueue queue => ctx -> queue
| ctx -> queue语法可以理解为“ctx暗示queue”。

现在,当您编写hasJobQueue <$> ask时,GHC已经知道ctx,并且知道它可以从queue中找出ctx。因此,代码不再是模棱两可的,它可以选择正确的实例。

当然,没有什么是免费的。功能上的依赖关系很好,但是我们要放弃什么呢?好吧,这意味着我们保证,对于每个ctx,仅存在一个queue,不会更多。没有功能依赖性,这两个实例可以共存:
instance HasJobQueue FooCtx MyQueueA
instance HasJobQueue FooCtx MyQueueB

这些完全合法,GHC将根据调用代码请求的队列类型来选择实例。对于功能依赖性,这是非法的,这是有道理的-整个观点是第二个参数必须由第一个参数隐含,并且如果有两个不同的选项,GHC不能仅由第一个参数来消除歧义。

从这个意义上说,功能依赖项允许类型类约束具有“输入”和“输出”参数。有时,功能依赖项被称为“类型级Prolog”,因为它们将约束求解器转换为关系子语言。这非常强大,甚至可以编写具有双向关系的类:
class Add a b c | a b -> c, a c -> b, b c -> a

但是,通常,功能依赖的大多数使用都涉及到您遇到的情况,其中一种结构在语义上“具有”关联类型。例如,经典示例之一来自mtl库,该库使用功能依赖项来表示读取器上下文,写入器状态等:
class MonadReader r m | m -> r
class MonadWriter w m | m -> w
class MonadState s m | m -> s
class MonadError e m | m -> e

这意味着可以使用关联的类型(TypeFamilies扩展的一部分)以稍有不同的方式等效地表示它们……但这可能超出了此答案的范围。

关于haskell - 类型约束最终变得模棱两可,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46714828/

相关文章:

haskell - G-machine,(非)严格上下文 - 为什么 case 表达式需要特殊处理

scala - 由正确类型限制的类型构造函数

c# - 如何在另一个泛型基类上添加 C# 泛型类型约束?

f# - 支持成员约束的静态扩展方法

swift - 如果 Swift 协议(protocol)是用类型约束定义的,为什么不能直接访问该类型的属性/方法?

haskell - 有理数的平方根

list - Haskell 中的置换实现

function - "func::String -> [Int]; func = read "有什么问题 [3,5,7 ]""

haskell - 使用 MTL 在 DSL 中分离关注点

haskell - 排名 n 约束? (或者,monad 转换器和 Data.Suitable)