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

时间:2017-10-12 16:39:52

标签: 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类的对象?

整个数据类型文件,包括我最终重新配置JobQueue以获得比上述内容更灵活的内容,是in this project

1 个答案:

答案 0 :(得分:5)

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

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

从类型检查器的角度来看,hasJobQueue是来自a -> 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或调用{{1} }}

那你怎么解决这个问题呢?好吧,一种方法是选择enqueue的特定类型,但根据你的代码,我打赌这实际上并不是你想要的。更有可能的是,有一个特定的类型的队列与每个特定的queue相关联,因此ctx的返回类型应该是暗示的第一个论点。幸运的是,Haskell有一个编码这个东西的概念,这个概念是功能依赖

请记住,我在开始时说过,约束通常不会影响类型推断?功能依赖性改变了这一点。当你编写一个fundep时,你声明类型检查器实际上可以从约束中获取信息,因为某些类型变量意味着其他一些变量。在这种情况下,您希望hasJobQueue暗示queue,因此您可以更改ctx的定义:

HasJobQueue

class HasJobQueue ctx queue | ctx -> queue where hasJobQueue :: JobQueue queue => ctx -> queue 语法可以理解为“| ctx -> queue隐含ctx”。

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

当然,没有什么是免费的。功能依赖很好,但我们放弃了什么?好吧,这意味着我们承诺,对于每个ctx完全一个ctx,不再存在。如果没有功能依赖,这两个实例都可以共存:

queue

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

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

instance HasJobQueue FooCtx MyQueueA
instance HasJobQueue FooCtx MyQueueB

但是,通常情况下,函数依赖项的大多数使用都涉及到您遇到的情况,其中一个结构在语义上“具有”关联类型。例如,其中一个经典示例来自mtl库,它使用函数依赖来表示读者上下文,编写器状态等:

class Add a b c | a b -> c, a c -> b, b c -> a

这意味着它们可以使用相关类型(class MonadReader r m | m -> r class MonadWriter w m | m -> w class MonadState s m | m -> s class MonadError e m | m -> e 扩展的一部分)以稍微不同的方式表达等等......但这可能超出了本答案的范围。