一个函数中的两个多态 类

Two polymorphic classes in one function

我有这个带有 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.

我可以概括 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 时,编译失败:

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"
   |   ^^^^^^^^^^^

我可以这样编译它,但它似乎与我试图概括的原始代码不同:

{-# 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"

非常感谢您的建议。谢谢!

您对 act :: Client c => String -> c a -> m a 的泛化尝试在技术上是正确的:它实际上是原始代码的翻译,但是将 State ModelData 替换为 m 并将 State ClientData 替换为 c.

错误发生是因为现在 "client" 可以是任何东西,scenario1 的调用者无法指定它应该是什么。

你看,为了确定要调用哪个版本的 addServer,编译器必须知道 c 是什么,但无处可推断! c 既不出现在函数参数中,也不出现在 return 类型中。所以从技术上讲,它绝对可以是任何东西,它完全隐藏在 scenario1 中。但是 "absolutely anything" 对编译器来说不够好,因为 c 的选择决定了调用哪个版本的 addServer,这将决定程序的行为。

这是同一问题的简化版本:

f :: String -> String
f str = show (read str)

这同样不会编译,因为编译器不知道要调用哪个版本的 showread


你有几个选择。

首先,如果scenario1本身知道使用哪个客户端,它可以使用TypeApplications:

这样说
scenario1 :: Model m => m ()
scenario1 = do
  act "Alice" $ addServer @(State ClientData) "https://example.com"

其次,scenario1可以将这个任务卸载给任何调用它的人。为此,您需要声明一个通用变量 c,即使它没有出现在任何参数或参数中。这可以通过 ExplicitForAll:

来完成
scenario1 :: forall c m. (Client c, Model m) => m ()
scenario1 = do
  act "Alice" $ addServer @c "https://example.com"

(请注意,您仍然需要执行 @c 让编译器知道要使用哪个版本的 addServer;要做到这一点,您需要 ScopedTypeVariables , 其中包括 ExplicitForAll)

那么消费者将不得不做这样的事情:

let server = scenario1 @(State ClientData)

最后,如果由于某种原因你不能使用TypeApplicationsExplicitForAllScopedTypeVariables,你可以做穷人的版本同样的事情——使用一个额外的虚拟参数来引入类型变量(这是以前的做法):

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"

(注意 class 方法本身现在也获得了一个虚拟参数;否则将无法再次调用它)

那么消费者将不得不做这件丑陋的事情:

let server = scenario1 (Proxy :: Proxy (State ClientData))