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以获得比上述更灵活的东西,在这个项目中
虽然很难根据给定的代码和上下文确切地知道问题的正确解决方案,但是您看到的错误源于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,并且知道它可以计算出queue从ctx.因此,代码不再模糊,它可以选择正确的实例.
当然,没有什么是免费的.功能依赖很好,但我们放弃了什么?嗯,这意味着我们承诺,对于每个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扩展的一部分)以略微不同的方式等效表达......但这可能超出了本答案的范围.