Есть ли инъекционный эквивалент для Haskell в контексте свободных монад

Я пытаюсь перевести это Скала cats пример о создании бесплатных монад.

Суть этого примера заключается в разложении отдельных задач на отдельные типы данных:

data Interact a = Ask (String -> a) | Tell String a deriving (Functor)

data DataOp = AddCat String | GetAllCats [String] deriving (Functor)

type CatsApp = Sum Interact DataOp

Не имея этих двух отдельных проблем, я бы построил "язык" для Interact операции следующим образом:

ask :: Free Interact String
ask = liftF $ Ask id

tell :: String -> Free Interact ()
tell str = liftF $ Tell str ()

Однако, если я хочу использовать ask а также tell в программе, которая также использует DataOp Я не могу определить их с типами выше, так как такая программа будет иметь тип:

program :: Free CatsApp a

В cats для определения tell а также ask Операции, которые они используют InjectK класс и inject метод из Free:

class Interacts[F[_]](implicit I: InjectK[Interact, F]) {
  def tell(msg: String): Free[F, Unit] = Free.inject[Interact, F](Tell(msg))
  def ask(prompt: String): Free[F, String] = Free.inject[Interact, F](Ask(prompt))
}

Что меня озадачивает то, что как-то позиционная информация о функторах (DataOp слева, Interact справа), кажется, не имеет значения (что довольно приятно).

Существуют ли похожие классы типов и функции, которые можно было бы использовать для элегантного решения этой проблемы с использованием Haskell?

1 ответ

Решение

Это описано в Типах данных по меню. Библиотека Scala, которую вы продемонстрировали, выглядит как довольно точный перевод оригинального Haskell. Суть в том, что вы пишете класс, представляющий отношения между функтором sup который "содержит" функтор sub:

class (Functor sub, Functor sup) => sub :-<: sup where
    inj :: sub a -> sup a

(В эти дни вы можете использовать Prism, потому что они могут как вводить, так и проецировать.) Затем вы можете использовать обычную технику составления базовых функторов ваших свободных монад с использованием функтора coproduct, реализовать :-<: для двух случаев Sum,

instance Functor f => f :-<: f where
    inj = id
instance (Functor f, Functor g) => f :-<: (Sum f g) where
    inj = InL
instance (Functor f, Functor g, Functor h, f :-<: g) => f :-<: (Sum h g) where
    inj = InR . inj

и сделать наборы команд компонуемыми, абстрагируясь над базовым функтором.

ask :: Interact :-<: f => Free f String
ask = liftF $ inj $ Ask id

tell :: Interact :-<: f => String -> Free f ()
tell str = liftF $ inj $ Tell str ()

addCat :: DataOp :-<: f => String -> Free f ()
addCat cat = liftF $ inj $ AddCat cat ()

getCats :: DataOp :-<: f => Free f [String]
getCats = liftF $ inj $ GetCats id

Теперь вы можете написать программу, которая использует оба Interact а также DataOp без ссылки на конкретный тип суммы.

myProgram :: (Interact :-< f, DataOp :-< f) => Free f ()
myProgram = ask >>= addCat

Однако все это не лучше, чем стандартный подход MTL.

Другие вопросы по тегам