Есть ли инъекционный эквивалент для 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.