一个函数中有两个多态类 [英] Two polymorphic classes in one function

查看:119
本文介绍了一个函数中有两个多态类的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在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替换为mState ClientDatac.

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)

由于编译器不知道要调用哪个版本的showread,因此同样无法编译.

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)

最后,如果由于某些原因您不能使用TypeApplicationsExplicitForAllScopedTypeVariables,则可以使用同一个人的可怜人的版本-使用额外的虚拟参数来引入类型变量(这是以前的方式):

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆