Два полиморфных класса в одной функции

У меня есть этот код с монадами состояния:

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"

Буду очень признателен за ваш совет. Спасибо!

1 ответ

Решение

Ваша попытка обобщения с act :: Client c => String -> c a -> m a технически правильно: это буквально перевод исходного кода, но заменяющий State ModelData с участием m а также State ClientData с участием c.

Ошибка возникает из-за того, что теперь, когда "клиентом" может быть что угодно, вызывающий scenario1 не имеет возможности указать, что это должно быть.

Видите ли, чтобы определить, какая версия addServer для вызова компилятор должен знать, что c есть, но выводить из этого некуда! cне появляется ни в параметрах функции, ни в возвращаемом типе. Так что технически это может быть что угодно, это полностью спрятано внутриscenario1. Но компилятору недостаточно "абсолютно ничего", потому что выборc определяет, какая версия addServer вызывается, который затем определяет поведение программы.

Вот уменьшенная версия той же проблемы:

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

Это также не будет компилироваться, потому что компилятор не знает, какие версии show а также read звонить.


У вас есть несколько вариантов.

Во-первых, если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)

Наконец, если по какой-то причине вы не можете использоватьTypeApplications, ExplicitForAll, или ScopedTypeVariables, вы можете сделать то же самое для бедняков - использовать дополнительный фиктивный параметр, чтобы ввести переменную типа (вот как это делалось раньше):

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"

(обратите внимание, что сам метод класса теперь также получил фиктивный параметр; иначе снова не будет возможности вызвать его)

Тогда потребителю придется сделать такую ​​мерзкую вещь:

let server = scenario1 (Proxy :: Proxy (State ClientData))
Другие вопросы по тегам