Переводчики для двух классов polymorphi c в одной функции - PullRequest
0 голосов
/ 22 марта 2020

У меня есть этот полиморфный c код (см. этот вопрос ) с обобщенными c монадами для модели и клиента:

import Control.Monad.Writer

class Monad m => Model m where
  act :: Client c => String -> c a -> m a

class Monad c => Client c where
  addServer :: String -> c ()

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

, и это интерпретатор с симпатичной печатью для Client, который объясняет действия в журнале через монаду Writer:

type Printer = Writer [String]

instance Client Printer where
  addServer :: String -> Printer ()
  addServer srv = tell ["  add server " ++ srv ++ "to the client"]

Переводчик для Model затруднен. Я пробовал несколько вещей, каждая из которых приводила к собственной ошибке:

  1. «Не удалось найти тип« c »»:
instance Model Printer where
  act :: String -> Printer a -> Printer a
  act name action = do
    tell [name ++ ":"]
    action
"Невозможно применить выражение типа Printer a к видимому аргументу типа (Printer a)":
instance Model Printer where
  act :: forall a. String -> Printer a -> Printer a
  act name action = do
    tell [name ++ ":"]
    action @(Printer a)
«Не удалось сопоставить тип« c »с« WriterT [String] Data.Functor.Identity.Identity »»
instance Model Printer where
  act :: Client c => String -> c a -> Printer a
  act name action = do
    tell [name ++ ":"]
    action

Каким-то образом мне нужно сказать, что было c a в act теперь Printer a.

Возможно, мне нужно иметь два параметра в классе Model - m для монады Model и c для монады Client, и класс Model также должен определять функцию clientToModel :: c a -> m a?

Есть ли способ развязать модель и клиента? Мне, вероятно, все еще нужно clientToModel :: c a -> m a для каждой пары?

Я ценю совет. Спасибо!

1 Ответ

1 голос
/ 23 марта 2020

Проблема в том, что подпись типа act обещает, что она будет работать на любом клиенте, но здесь вы пытаетесь ограничить ее работу только на указанном c клиенте, называемом Printer. Это нарушает определение класса Model.


Обычный шаблон, которому вы, очевидно, пытаетесь следовать, - это определение Model и Client в одной и той же монаде, например так :

class Monad m => Model m where
  act :: String -> m a -> m a

class Monad m => Client m where
  addServer :: String -> m ()

У этого есть хорошая, легко понятная семантика, что и act и addServer являются операциями "окружающего контекста", которые "доступны в монаде m". Они почти похожи на «глобальные функции», но все еще могут быть поддельными.

Тогда Printer может быть одним из примеров такой монады, реализующей как Client, так и Model. И тогда ваш производственный стек - например, ReaderT Config IO или что-то еще у вас - может быть другим примером такой монады.


Однако, если вы настаиваете на определении Model и Client для разных монад единственный способ заставить работать типы - это снять ограничение Client c с сигнатуры act до сигнатуры класса Model:

class (Monad m, Client c) => Model m c where
  act :: String -> c a -> m a

Что будет иметь значение " каждая "модель" монада работает с определенным набором "клиентских" монад, но не только с любой случайной "клиентской" монадой".

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

instance Model Printer Printer where
  act name action = do
    tell [name ++ ":"]
    action

И типы будут работать.


Сказав это, я еще раз хочу повторить, что ваше решение определить Client и Model для разных монад это запах для меня. Я настоятельно рекомендую вам пересмотреть свой дизайн, как предложено выше.

...