在我正在研究的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
答案 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
扩展的一部分)以稍微不同的方式表达等等......但这可能超出了本答案的范围。