I'm trying to translate this Scala's cats
example about composing free monads.
The gist of the example seems to be the decomposition of separate concerns into separate data types:
data Interact a = Ask (String -> a) | Tell String a deriving (Functor)
data DataOp = AddCat String | GetAllCats [String] deriving (Functor)
type CatsApp = Sum Interact DataOp
Without having these two separate concerns, I would build the "language" for Interact
operations as follows:
ask :: Free Interact String
ask = liftF $ Ask id
tell :: String -> Free Interact ()
tell str = liftF $ Tell str ()
However, if I want to use ask
and tell
in a program that also uses DataOp
I cannot define them with the types above, since such a program will have type:
program :: Free CatsApp a
In cats
, for the definition of the tell
and ask
operations they use an InjectK
class, and an inject
method from 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))
}
What puzzles me is that somehow the positional information of the functors ( DataOp
is on the left, Interact
is on the right) seems to be irrelevant (which is quite nice).
Are there similar type-classes and functions that could be used to solve this problem in an elegant manner using Haskell?
This is covered in Data Types à la Carte . The Scala library you demonstrated looks like a fairly faithful translation of the original Haskell. The gist of it is, you write a class representing a relationship between a functor sup
which "contains" a functor sub
:
class (Functor sub, Functor sup) => sub :-<: sup where
inj :: sub a -> sup a
(These days you might use a Prism
, because they can both inject and project.) Then you can use the usual technique of composing the base functors of your free monads using the functor coproduct, implement :-<:
for the two cases of 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
and make instruction sets composable by abstracting over the base functor.
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
Now you can write a program which uses both Interact
and DataOp
without making reference to a particular sum type.
myProgram :: (Interact :-< f, DataOp :-< f) => Free f ()
myProgram = ask >>= addCat
None of this is particularly better than the standard MTL approach, though.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.