parsonsmatt / abstract-effects

A package for abstract effects in Haskell
BSD 3-Clause "New" or "Revised" License
2 stars 0 forks source link

Package features #1

Open ElvishJerricco opened 8 years ago

ElvishJerricco commented 8 years ago

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.

{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}

import qualified Control.Category as C
import qualified Prelude as P
import           Prelude hiding (id, (.))

newtype Interpreter a b = MkInterpreter
  { interpret :: forall n x. b n => (forall m. a m => m x) -> n x }

instance Category Interpreter where
  id = MkInterpreter P.id
  MkInterpreter f . MkInterpreter g = MkInterpreter $ \h -> f (g h)

(Note: Prelude.(.) does not cooperate with constrained rank n types, so composition of f and g has to be manual)

I suppose the purpose of this package should be utilities surrounding this paradigm of composing interpreters.

ElvishJerricco commented 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?

parsonsmatt commented 8 years ago

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.

ElvishJerricco commented 8 years ago

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?

parsonsmatt commented 8 years ago

Yeah -- definitely something like the Pipes tutorial or Control.Lens.Tutorial!

ElvishJerricco commented 8 years ago

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
parsonsmatt commented 8 years ago

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 Functors on them.

parsonsmatt commented 8 years ago

Also, I think there may be some prior art here from Conal Elliot's Type Class Morphisms which I'm digging into right now

ElvishJerricco commented 8 years ago

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.

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!

parsonsmatt commented 8 years ago

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.

ElvishJerricco commented 8 years ago

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.

ElvishJerricco commented 8 years ago

Aw. The functor class we would want can't be derived with GHC Generics =/ The Generic1 class only works for types of kind * -> *.

parsonsmatt commented 8 years ago

There's always Template Haskell :smile:

ElvishJerricco commented 8 years ago

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.

parsonsmatt commented 8 years ago

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.

ElvishJerricco commented 8 years ago

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.

parsonsmatt commented 8 years ago

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.

ElvishJerricco commented 8 years ago

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"?

parsonsmatt commented 8 years ago

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

ElvishJerricco commented 8 years ago

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.

parsonsmatt commented 8 years ago

You're right. I'd attempted to play around with that earlier and didn't have any success with that approach.

ElvishJerricco commented 8 years ago

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.
ElvishJerricco commented 8 years ago

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.

ElvishJerricco commented 8 years ago

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.