Ваша попытка обобщения с 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
может переложить эту задачу на любого, кто ее вызывает. Для этого вам нужно объявить переменную generic c 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
, вы можете сделать то же самое для версии для бедного человека - использовать дополнительный фиктивный параметр, чтобы ввести переменную типа (это было сделано ранее 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"
(обратите внимание, что сам метод класса теперь также получил фиктивный параметр; в противном случае его снова не удастся вызвать)
Тогда потребителю придется сделай эту уродливую вещь:
let server = scenario1 (Proxy :: Proxy (State ClientData))