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

Sav*_*nel 3 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
    ...
Run Code Online (Sandbox Code Playgroud)

所以,对我而言,这似乎很清楚.在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’
Run Code Online (Sandbox Code Playgroud)

这是我正在尝试构建的一个示例,这个来自我的API测试套件,我用简单的IORef模拟所有后端,其中生产将有其他后端实现

data    MemoryCloister  = MemoryCloister WorkBuffer
newtype WorkBuffer      = WorkBuffer (IORef [WorkItem Command]) 

instance JobQueue WorkBuffer where 
    hasJobQueue (MemoryCloister wb) = wb

instance JobQueue WorkBuffer where
    ... 
Run Code Online (Sandbox Code Playgroud)

那么,我究竟需要做些什么才能帮助类型检查器理解MonadReader包含实现JobQueue类的对象的上下文?


整个数据类型文件,包括我最终如何重新配置​​JobQueue以获得比上述更灵活的东西,在这个项目中

Ale*_*ing 5

虽然很难根据给定的代码和上下文确切地知道问题的正确解决方案,但是您看到的错误源于HasJobQueue类型类,这是非常通用的:

class HasJobQueue ctx queue where
  hasJobQueue :: JobQueue queue => ctx -> queue
Run Code Online (Sandbox Code Playgroud)

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

如果这令人困惑,请考虑一个与typechecker几乎相同的略有不同的类:

class Convert a b where
  convert :: a -> b
Run Code Online (Sandbox Code Playgroud)

这个类型类通常是一个反模式(正是因为它使类型推断非常困难),但它理论上可以用来提供实例来转换任何两种类型.例如,可以编写以下实例:

instance Convert Integer String where
  convert = show
Run Code Online (Sandbox Code Playgroud)

...然后用于convert将整数转换为字符串:

ghci> convert (42 :: Integer) :: String
"42"
Run Code Online (Sandbox Code Playgroud)

但是,请注意,以下将工作:

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.
Run Code Online (Sandbox Code Playgroud)

这里的问题是GHC不知道b应该是什么,所以它不能选择Convert使用哪个实例.

在你的代码中,hasJobQueue虽然细节稍微复杂一些,但却大致相同.问题出现在以下几行:

queue <- hasJobQueue <$> ask
enqueue (SaveDocument addr document) queue
Run Code Online (Sandbox Code Playgroud)

为了知道HasJobQueue要使用哪个实例,GHC需要知道其类型queue.好吧,幸运的是,GHC可以根据它们的使用方式推断绑定类型,所以希望queue可以推断出类型.它作为第二个参数提供enqueue,因此我们可以通过查看以下类型来了解正在发生的事情enqueue:

enqueue :: (JobQueue q, MonadIO m) => Command -> q -> m ()
Run Code Online (Sandbox Code Playgroud)

在这里我们看到了问题.第二个参数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
Run Code Online (Sandbox Code Playgroud)

| ctx -> queue语法可以被解读为" ctx暗示queue".

现在,当你写hasJobQueue <$> ask,GHC已经知道ctx,并且知道它可以计算出queuectx.因此,代码不再模糊,它可以选择正确的实例.


当然,没有什么是免费的.功能依赖很好,但我们放弃了什么?嗯,这意味着我们承诺,对于每个ctx,有恰好一个queue,没有更多的.没有功能依赖,这两个实例可以共存:

instance HasJobQueue FooCtx MyQueueA
instance HasJobQueue FooCtx MyQueueB
Run Code Online (Sandbox Code Playgroud)

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

从这个意义上讲,函数依赖性允许类型类约束具有"输入"和"输出"参数.有时,函数依赖被称为"类型级别Prolog",因为它们将约束求解器转换为关系子语言.这非常强大,您甚至可以编写具有双向关系的类:

class Add a b c | a b -> c, a c -> b, b c -> a
Run Code Online (Sandbox Code Playgroud)

但是,通常情况下,函数依赖项的大多数使用都涉及到您遇到的情况,其中一个结构在语义上"具有"关联类型.例如,其中一个经典示例来自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
Run Code Online (Sandbox Code Playgroud)

这意味着它们可以使用相关类型(TypeFamilies扩展的一部分)以略微不同的方式等效表达......但这可能超出了本答案的范围.