一个函数中有两个多态类 [英] Two polymorphic classes in one function
问题描述
我在State monads中有以下代码:
I have this code with State monads:
import Control.Monad.State
data ModelData = ModelData String
data ClientData = ClientData String
act :: String -> State ClientData a -> State ModelData a
act _ action = do
let (result, _) = runState action $ ClientData ""
return result
addServer :: String -> State ClientData ()
addServer _ = return ()
scenario1 :: State ModelData ()
scenario1 = do
act "Alice" $ addServer "https://example.com"
我正在尝试按照这种方法使用多态类型类对其进行概括: https://serokell. io/blog/tagless-final .
I am trying to generalise it with polymorphic type-classes following this approach: https://serokell.io/blog/tagless-final.
我可以概括ModelData:
I can generalise ModelData:
import Control.Monad.State
class Monad m => Model m where
act :: String -> State c a -> m a
data Client = Client String
addServer :: String -> State Client ()
addServer _ = return ()
scenario1 :: Model m => m ()
scenario1 = do
act "Alice" $ addServer "https://example.com"
但是当我尝试同时使用ModelData和ClientData时,它无法编译:
But when I try to do it with both ModelData and ClientData it fails to compile:
module ExampleFailing where
class Monad m => Model m where
act :: Client c => String -> c a -> m a
class Monad c => Client c where
addServer :: String -> c ()
scenario1 :: Model m => m ()
scenario1 = do
act "Alice" $ addServer "https://example.com"
错误:
• Could not deduce (Client c0) arising from a use of ‘act’
from the context: Model m
bound by the type signature for:
scenario1 :: forall (m :: * -> *). Model m => m ()
at src/ExampleFailing.hs:9:1-28
The type variable ‘c0’ is ambiguous
• In the expression: act "Alice"
In a stmt of a 'do' block:
act "Alice" $ addServer "https://example.com"
In the expression:
do act "Alice" $ addServer "https://example.com"
|
11 | act "Alice" $ addServer "https://example.com"
| ^^^^^^^^^^^
我可以用这种方式进行编译,但是它似乎与我试图概括的原始代码不同:
I can make it compile this way, but it seems different from the original code I am trying to generalise:
{-# LANGUAGE MultiParamTypeClasses #-}
module ExamplePassing where
class Monad m => Model m c where
act :: Client c => String -> c a -> m (c a)
class Monad c => Client c where
addServer :: String -> c ()
scenario1 :: (Client c, Model m c) => m (c ())
scenario1 = do
act "Alice" $ addServer "https://example.com"
非常感谢您的建议.谢谢!
I would really appreciate your advice. Thank you!
推荐答案
您使用act :: Client c => String -> c a -> m a
进行的泛化尝试在技术上是正确的:从字面上看,它是原始代码的翻译,但是将State ModelData
替换为m
和State ClientData
与c
.
Your generalization attempt with act :: Client c => String -> c a -> m a
is technically correct: it's literally a translation of the original code, but replacing State ModelData
with m
and State ClientData
with c
.
发生错误是因为现在客户端"可以是任何东西,scenario1
的调用者无法指定它应该是什么.
The error happens because now that the "client" can be anything, the caller of scenario1
has no way to specify what it should be.
您看到,为了确定要调用的addServer
版本,编译器必须知道c
是什么,但是没有地方可以从中推断出该内容! c
既不出现在函数参数中,也不出现在返回类型中.因此,从技术上讲,它绝对可以是任何东西,它完全隐藏在scenario1
内部.但是绝对没有"对于编译器来说还不够好,因为选择c
决定了调用哪个版本的addServer
,这随后将确定程序的行为.
You see, in order to determine which version of addServer
to call, the compiler has to know what c
is, but there is nowhere to infer that from! c
appears neither in the function parameters nor in return type. So technically it can be absolutely anything, it's completely hidden inside scenario1
. But "absolutely anything" isn't good enough for the compiler, because the choice of c
determines which version of addServer
is called, which will then determine the program behavior.
以下是该问题的小版本:
Here's a smaller version of the same problem:
f :: String -> String
f str = show (read str)
由于编译器不知道要调用哪个版本的show
和read
,因此同样无法编译.
This will similarly not compile because the compiler doesn't know which versions of show
and read
to call.
您有一些选择.
首先,如果scenario1
本身知道要使用哪个客户端,则可以使用TypeApplications
这样说:
First, if scenario1
itself knows which client to use, it can say so by using TypeApplications
:
scenario1 :: Model m => m ()
scenario1 = do
act "Alice" $ addServer @(State ClientData) "https://example.com"
第二, scenario1
可以将此任务卸载给任何调用它的人.为此,即使它没有出现在任何参数或参数中,也需要声明一个通用变量c
.这可以通过ExplicitForAll
:
Second, scenario1
can offload this task onto whoever calls it. To do that, you need to declare a generic variable c
even though it doesn't appear in any parameters or arguments. This can be done with ExplicitForAll
:
scenario1 :: forall c m. (Client c, Model m) => m ()
scenario1 = do
act "Alice" $ addServer @c "https://example.com"
(请注意,您仍然必须执行@c
以便让编译器知道要使用哪个版本的addServer
;要执行此操作,您将需要ScopedTypeVariables
,其中包括ExplicitForAll
)
(note that you still have to do @c
to let the compiler know which version of addServer
to use; to be able to do this, you'll need ScopedTypeVariables
, which includes ExplicitForAll
)
然后,消费者将必须执行以下操作:
Then the consumer will have to do something like this:
let server = scenario1 @(State ClientData)
最后,如果由于某些原因您不能使用TypeApplications
,ExplicitForAll
或ScopedTypeVariables
,则可以使用同一个人的可怜人的版本-使用额外的虚拟参数来引入类型变量(这是以前的方式):
Finally, if for some reason you cannot use TypeApplications
, ExplicitForAll
, or ScopedTypeVariables
, you can do the poor man's version of the same thing - use an extra dummy parameter to introduce the type variable (this is how it was done in the before times):
class Monad c => Client c where
addServer :: Proxy c -> String -> c ()
scenario1 :: (Client c, Model m) => Proxy c -> m ()
scenario1 proxyC = do
act "Alice" $ addServer proxyC "https://example.com"
(请注意,类方法本身现在也已获取了一个虚拟参数;否则将再次无法调用它)
(note that the class method itself has now also acquired a dummy parameter; otherwise there will again be no way to call it)
然后,消费者将不得不做这个丑陋的事情:
Then the consumer will have to do this ugly thing:
let server = scenario1 (Proxy :: Proxy (State ClientData))
这篇关于一个函数中有两个多态类的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!