Open ElvishJerricco opened 8 years ago
Relatively unimportant, but I figure this has more to do with interpreters of effects than effects themselves, so maybe abstract-interpreters
would be a more apt name?
Yeah, I think that's the end goal. I'd like for this to be a "core" package that implements the overall framework, with separate packages around eg wreq
, persistent
, etc. that can implement those effects.
I'm going to be developing these ideas in work projects where I'll get a good idea of exactly what sorts of conveniences this package could offer. Right now, the newtype
and the Category
instance are all I can think of. Template Haskell or Generics stuff to help with boilerplate may be nice, though in all honesty GeneralizedNewtypeDeriving
will get this a lot of the way there.
Yea I was thinking along the same lines. I suppose common patterns will show up as it gets used and we can figure out how to get stuff in the package. In the meantime, the category is the central piece.
So I guess the majority of this repo should be deicated to documenting the idea. So maybe include a link to your blog post, along with a readme that regurgitates the ideas from the blog post but specialized to this category instance?
Yeah -- definitely something like the Pipes tutorial or Control.Lens.Tutorial
!
I was thinking about the Services
example you gave, and subsequently a version of it that uses this category.
data Services c = Services
{ runHttp :: Interpreter MonadHttp c
, runPersist :: Interpreter MonadHttp c
}
and I think I stumbled on a potential abstraction.
mapServices :: Interpret c d -> Services c -> Services d
mapServices interpreter services = services
{ runHttp = interpreter . runHttp
, runPersist = interpreter . runPersist
}
Surely this abstracts into a class of some kind? And surely that class is derivable via generics?
class InterpretFunctor f where
inmap :: Interpret c d -> f c -> f d
Hmm, yeah! This reminds me of "The functor design pattern" and the previous post in that series. If we've got a Category
then surely we can find some useful Functor
s on them.
Also, I think there may be some prior art here from Conal Elliot's Type Class Morphisms which I'm digging into right now
Hm. In a categorical sense, functor could be defined like this.
class (Category c, Category d) => Functor c d f where
fmap :: c a b -> d (f a) (f b)
The main point being the kinds of these types.
c :: k1 -> k1 -> *
d :: k2 -> k2 -> *
f :: k1 -> k2
f
is a morphism of kinds. In the InterpretFunctor
class I defined above, it all looks something like this:
inmap :: (c ~ Interpret, d ~ (->)) => c a b -> d (f a) (f b)
So yea, Services
is a functor from Interpret
to (->)
. Cool!
We could get real fancy and use an HList
-like-thing of constraints instead of a concrete data type, and then instead of Services g
we'd be talking about Elem MonadHttp cs => Services cs
, etc . . . but I don't know if the additional implementation/type complexity is worth it when strict fields get you there.
Yea I don't see a compelling reason to do that. I think it's just easier to reason about functors from Interpret
to (->)
and worry about concrete implementations as a trivialty.
To that end, is there an existing functor class we can use for this? I know the hask
library has a more complete definition of functors, but it's almost too complete =P It's a much more complex class than the one we need.
Aw. The functor class we would want can't be derived with GHC Generics =/ The Generic1
class only works for types of kind * -> *
.
There's always Template Haskell
:smile:
Well still, I'm not sure what functor class we want to use. We can either specialize and do this:
class InterpretFunctor f where
inmap :: Interpret a b -> f a -> f b
instance InterpretFunctor Services where
...
Or we can be more general and do this:
class (Category dom, Category cod) => CategoryFunctor dom cod f where
cmap :: dom a b -> cod (f a) (f b)
instance CategoryFunctor Interpret (->) Services where
...
The latter is more general, but feels out of scope for this package. But I don't know of any package on Hackage that has this class already.
How useful is the InterpretFunctor
class going to be? I don't imagine having many Services
like records around, probably just a single one per application. I also am not sure how frequently I'll want to be mapping another interpreter over all of the fields in a record -- i imagine most Interpret
types will be going to MonadIO
or similar in the final construction of the record, and the composition is mostly useful for building layers on top of other services.
Well I think of it like this: A lot of operations are going to be a hierarchy of effects that could be isolated. Networking is a prime example of this.
MonadIO
└─ MonadNetwork
├─ MonadHttp
│ └─ MonadRestApi
└─ MonadSql
└─ MonadMyDbOps
By making Services
a functor, we can choose how far up this hierarchy we want to go. We can base an application on Services MonadHttp
, then in main
use it as a functor to turn it into Services MonadIO
to actually run it. Now in test code, we can use the functor to make Services MonadMock
or whatever.
I can see the appeal to that.
One issue that the newtype
brings is that we can't use it nicely in apps anymore. If all the services are in newtypes, then the code:
foobar Services{..} = do
page <- runHttp (get "asdf")
pure (length page)
has to be translated to:
foobar Services{..} = do
page <- interpret (runHttp (get "adsf"))
pure (length page)
The type of the thing in the interpreter is just a function, so it may make more sense to export a compatible .
operator.
Almost =P It'd be
foobar Services {..} = do
page <- interpret runHttp (get "asdf")
pure (length page)
Which is a little nicer, though not much.
What do you mean by "export a compatible .
operator"?
Lol I'm worthless without a compiler
Control.Category..
works with the constraints and stuff, and $
works fine (probably due to the magic). I imagine that it wouldn't be impossible to provide .
that worked with these transformations if it were well typed enough
Note that this:
type Interpret a b = forall n x. b n => (forall m. a m => m x) -> n x
f :: Interpret b c -> Interpret a b -> Interpret a c
f = (.)
produces errors in the type signature. It fails to understand the type signature just due to the complexity of the constraints. It never even tries to type check the term. No amount of well typing in some (.)
is going to make up for this error, and I imagine it's one we'd rather avoid.
You're right. I'd attempted to play around with that earlier and didn't have any success with that approach.
Another note, just because I feel this is worth documenting: This category rightfully has no product types. The reason we would want product types would be to interpret multiple constraints at once trivially.
-- pseudo-code
f :: Interpret (MonadHttp, MonadSql) MonadIO
But the laws of products in a category require these morphisms:
A × B -> A
A × B -> B
This isn't possible with this category. Interpret (MonadHttp, MonadSql) MonadHttp
can't sensibly exist; there's no way to guarantee the existence of such an interpreter. Thus, multiple constraints have to be explicitly composed. And this makes sense; as monads can't be automatically composed, it makes sense that you can't automatically compose monad classes. As with monads in general, the composition needs to be manual. With this category, this would be accomplished either using a functor (like Services
), or using a custom aggregating class.
class (MonadHttp m, MonadSql m) => MonadNet m
f :: Interpret MonadNet MonadIO
f = MkInterpret $ \x -> ... -- interpretation of composition of monads is manual, as expected.
Aww... Someone made Generic1
poly-kinded, and now it's in GHC HEAD. Which is awesome, but it's not in base-4.9.0.0
so we can't use it =( If we had that, the InterpretFunctor
class would go from debatable to obvious, since we could derive it.
Just pushed a first-draft
branch. Tell me what you think! Don't mind the name change; that was just the name I found more fitting. No need to keep it.
The core desired effect is the polymorphic composition of interpreters. That is, given
mtl
-style classes, arrows between them that can choose how to interpret a class.(Note:
Prelude.(.)
does not cooperate with constrained rank n types, so composition off
andg
has to be manual)I suppose the purpose of this package should be utilities surrounding this paradigm of composing interpreters.