polysemy-research / polysemy-zoo

:monkey::panda_face: Experimental, user-contributed effects and interpreters for polysemy
BSD 3-Clause "New" or "Revised" License
70 stars 21 forks source link

Ported Random from polysemy, added RandomFu. #3

Closed adamConnerSax closed 5 years ago

adamConnerSax commented 5 years ago

Also: Updated package.yaml and regenerated polysemy-zoo.cabal. Added some things to gitignore for cabal v2-style builds and emacs TAGS files.

Questions:

  1. Are we comfortable with the orphan instance of "MonadRandom"? I think it will be useful but it's somewhat against the spirit of polysemy's mtl-free-ness and it's an Orphan.

  2. I don't see an easy way to implement these as the same effect and then interpret but I didn't look that hard. I can imaging Random being implemented in terms of RandomFu but not the other way around. And that's a commitment to a less standard external dependency.

  3. It would be possible, but, I think, a bit more work to write the equivalent of runRandom for RandomFu. random-fu has instances of things in terms of State StdGen but those are built around the mtl State and they'd have to be re-written, which would mean more orphan instances. I think.

  4. Should there be tests?

isovector commented 5 years ago

To answer your questions:

I'd love a pure runRandomFu function. Rather than introducing the orphan, you can almost certainly get it by reinterpreting RandomFu into Lift (Control.Monad.State whateverthingyouneed), and then handling that thing explicitly.

Tests would be good -- if only to show how to use the thing.

adamConnerSax commented 5 years ago

Fixed the simple stuff. Renaming, LambdaCase, removing Random. I'll have to think more about the MonadRandom/instance stuff. So that'll have to wait a bit...

What do you mean in the above by "get it by"? What would I do with a function that has a MonadRandom constraint and which I would like to run in a Member RandomFu r => Sem r without the instance I provided?

isovector commented 5 years ago

What do you mean in the above by "get it by"? What would I do with a function that has a MonadRandom constraint and which I would like to run in a Member RandomFu r => Sem r without the instance I provided?

There are Monad m => MonadRandom (StateT StdGen m) instances; so I don't see why we couldn't reinterpret a Sem (RandomFu ': r) a into a Sem (State StdGen ': r) a into a StateT StdGen (Sem r) a into Sem r (StdGen, a).

The Sem (State s ': r) a -> StateT s (Sem r) a transformation might be tricky, but I don't see any reason it couldn't be done.

isovector commented 5 years ago

hoistStateIntoStateT

adamConnerSax commented 5 years ago

Nice. That would've taken me...a while.

adamConnerSax commented 5 years ago

So. I get the runRandomFu thing and, once there is a released version of polysemy with the hoistStateIntoStateT, I'll fix up the PR.

But I still don't get what your mean about the instance/constraint thing. You may need to explain it to me like I am a (Haskell-knowing) five-year-old. Suppose I have a function:

doSomethingRandom :: MonadRandom m => m a

and I want to be able to call that function inside

myEffectfulFunction :: Member RandomFu r => (a -> b) -> Sem r b
myEffectfulFunction f = do
   a <- ?? doSomethingRandom
   return $ f a

either by having instance Member RandomFu r => MonadRandom (Sem r) or what? I guess a function of the form liftRandomFu :: (Member RandomFu r, MonadRandom m) => m a -> Sem r a ?? I don't quite see how hoisting to StateT gives us the former. Am I missing something? And the latter, a function which discharges the constraint in the presence of Member RandomFu r certainly seems possible, but also sub-optimal for ease of use. Is that what you have in mind? Sort of like liftIO? I guess I can see how that might be the right balance when trying to interoperate with mtl but not become shot through with it.

isovector commented 5 years ago

I was under the impression that the MonadRandom bit was necessary to run any of the random-fu stuff. If it's actually just for convenience of interoping with code that already uses MonadRandom, then yes, I agree, let's just burn the bridge and not support any MonadRandom stuff.

adamConnerSax commented 5 years ago

Hmm. But I still want the interop! I have some data-science stuff that has that signature. Which I guess I could change but there's a lot more mtl style code out there and it might be worth creating a path. Let me think about the constraint-discharging version. That wouldn't infect the lib with an orphan instance but would provide a way for interop to be relatively painless.

isovector commented 5 years ago

Hiya, what's the status of this?

adamConnerSax commented 5 years ago

Hiya!

I guess I was waiting for a release with hoistStateIntoStateT and then I would try to move forward. Has that happened? I get confused trying to work against unreleased versions...

In the meantime I have been using Polysemy a bunch in knit-haskell and doing some fun Bayesian inference on voter preference using US House Election Results. And discovering a couple of usability things:

  1. (Without the plugin) Many errors become Ambiguous type errors. It's a little like TypeApplications. Any small thing breaks the inference and then the first error you get is always Polysemy.

  2. Trying to add the plugin adds a lower bound on containers of 0.6.0 (not sure why. Maybe the "installed" ghc version?) and that makes it unusable for me because knit-haskell has a "plots" dependency with an < 0.6.0 bound on containers. Sigh.

Anyway, happy to get back to it. Should I?

Adam

isovector commented 5 years ago
  1. Try disabling the custom-errors flag --- it'll stop the aggressive polysemy errors. It's an unfortunate side-effect of GHC's error machinery. Maybe I'll investigate generating those errors from the plugin, where it'd be much easier to not clobber the usual Haskell errors.
  2. Sorry about that, I needed to add explicit bounds for getting the package into stackage, and didn't feel like playing around to get a real set of bounds. If you can send a PR changing the package.yaml files for a better containers lower-bound I'll happy merge it and push a release ASAP.
isovector commented 5 years ago

As for hoistStateT, I think we came to the conclusion that it won't actually help with the MonadRandom instance.

adamConnerSax commented 5 years ago

I'll get back to it then! For now, I can work around the containers issue with an "allow-newer" in my cabal.project. Plots seems to compile fine with containers-0.6.

adamConnerSax commented 5 years ago

I fiddled with this for a bit and so far the only thing I can do is

absorbMonadRandom
  :: Member RandomFu r => (forall m . R.MonadRandom m => m a) -> Sem r a

(just via an instance on a newtype wrapper) which gets us some of the way but not as far as I'd like. But I don't think we can do the more general

absorbMonadRandom :: (MonadRandom m, Member RandomFu r) => m a -> Sem r a

because we can't the a out of that monad, right? The case above works because of the Rank2-ness of the 'm' so the absorbMonadRandom function gets to choose to implement m as the wrapped Sem with the instance, thus discharging the constraint. But the more I think about it, the less clear I get on how we could do more, especially without violating the spirit of Polysemy--to not prematurely foreclose on our possible interpretations.

I don't know how useful this thing will be. I'm going to try and find some places to use it and see if it's helpful .

And "absorb"? I don't know. It's not quite lift or hoist or raise, right?

Once I use it a bit, I'll try to add an example that makes it clearer how to use it and random-fu altogether. But that might need to wait until next week.

isovector commented 5 years ago

I really like the type of absorbMonadRandom. I think it's an elegant solution to the mtl interop problem. Maybe we could go further with constraint kinds:

class CanonicalEffect (c :: (* -> *) -> Constraint) (e :: (* -> *) -> * -> *) where
  absorb :: Member e r => (forall m. c m => m a) -> Sem r a

instance CanonicalEffect MonadRandom Random where
instance CanonicalEffect (MonadReader r) (Reader r) where

etc., although I'm not sure how nicely it would play if you have multiple mtl constraints you want to absorb simultaneously.

adamConnerSax commented 5 years ago

Thanks!

Yeah. But maybe single effects are enough? If you start having multiple effects you should re-write in polysemy or use polysemy on top or on the bottom of the stack, maybe? Let me make some examples for this one and then think a bit about the generalized version of "absorb". And multiple effects.

Would that class need a fundep (or an associated type family)? How would the compiler know what e you had in mind at the absorb call-site? Also, I wonder if all the absorbers are canonical? Is there ever a case where we might choose different effects to absorb the same mtl-constraint? For instance, if we could interpret random-fu via System.Random then we might want to absorb the MonadRandom constraint via Random instead of RandomFu. But maybe that's a bad example because the random-effects are specifically weird.

But the question stands. I'm not sure this generalization should be a typeclass though we can certainly generalize the signatures and add some helpers so that each specific absorber is more compact and uniform.

And I suppose you could write multi-effect absorbers. Not in the polysemy libraries, but for a specific application it might make sense as a way to use legacy mtl-code. So maybe the "absorb" kit should be built to make those easy to build when you want them? I'll think about that as well.

adamConnerSax commented 5 years ago

See https://github.com/isovector/polysemy-zoo/pull/8 for CanonicalEffect. I'll clean up this one once you decide how you want that to work...