Два полиморфных класса в одной функции
У меня есть этот код с монадами состояния:
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))