-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use the Has type class instead of explicit monad transformer stacks #103
Comments
This would be a cool experiment to see whether |
But if the value is qualified by a constraint, e.g. |
Ok, gave it a try, and indeed the case I mentioned doesn't seem to need lift: {-# LANGUAGE FlexibleContexts #-}
module Main where
import Control.Monad.Trans.Has.Reader
( HasReader,
ReaderT (runReaderT),
ask,
)
import Control.Monad.Trans.Has.State
( HasState,
StateT (runStateT),
put,
)
import Lib
main :: IO ()
main = handled >>= print
a :: (Monad m, HasState Int m) => m ()
a = put (3 :: Int)
b :: (Monad m, HasState Int m, HasReader Int m) => m Int
b = do
put (4 :: Int)
a
ask
handled :: IO (Int, Int)
handled = b `runReaderT` (10 :: Int) `runStateT` 0 |
Ah right, if we change the type signatures of several functions, it would get even easier. I'd be open for a PR to try that out. |
I had a go at this. For exceptions: throwC
:: forall m e arbitrary . (Monad m, HasExcept e m)
=> Cell m e arbitrary
throwC = arrM (\e -> liftH $ ExceptT $ return $ Left e) -- can't avoid lambda, doesn't know the type of n The trade-off of using Has is that some additional type annotations are necessary as the full type has to be known for the compiler to determine which instance of Has to use. In some cases we don't care about the exception type e, and it is not known as it is thrown away, but with "Has" it has to be known, I think: throwWhen0
:: (Monad m, HasExcept () m)
=> Cell m Double Double
throwWhen0 = proc pos ->
if pos < 0
then throwC -< ()
else returnA -< pos
sineChangeE = do
try @() $ sine 6 >>> throwWhen0
try @() $ (constM $ lift $ putStrLn "I changed!")
>>> throwC
safe $ sine 10 Here, the exception type has to be explicitly determined (I used type application), which is very inconvenient... For HandlingStateT things are more complicated because HandlingStateT m is not really just a monad transformer, but a monad transformer which has type m which has to match the monad inside the transformer ( class HasMonad (inner :: * -> *) (outer :: * -> *) where
liftHM :: inner a -> outer a
instance Monad m => HasMonad m m where
liftHM = id
{-# INLINE liftHM #-}
instance {-# Overlappable #-} (Monad inner, Monad outer, MonadTrans t, HasMonad inner outer) => HasMonad inner (t outer) where
liftHM = lift . liftHM
{-# INLINE liftHM #-}
handling' :: (HasMonad (HandlingStateT n) m, Typeable h, Monad n) =>
Handle n h -> Cell m arbitrary h
handling' = hoistCell liftHM . handling The problem here is that the type n is usually unknown. Even activating AmbiguousTypes quickly one gets into a lot or problems. E.g.: readEventsC ::
forall m n arbitrary .
(MonadIO m, MonadIO n, HasExcept EOLCPortMidiError m, HasMonad (HandlingStateT n) m)
=> String -> Cell m arbitrary [PMEvent]
readEventsC name = proc _ -> do
pmStreamE <- handling' @n @m $ portMidiInputStreamHandle name -< ()
pmStream <- exceptC -< pmStreamE
readEventsFrom4 -< pmStream Here we have to add type application to handing' so that it knows what is the n and m monads. I think it would work well if it was Perhaps there is a way around these issues, but if there isn't then it seems that the approach with type classes probably creates more problems than it solves... :-( |
But it should suffice to give
Another way around this might be: type HasHandling m = HasState (HandlingState m) This means that the
Thanks a lot for the thorough investigation! Well, we don't have to use |
Unfortunately no, because doing that only binds the last value of the monad it doesn't bind the intermediate values in the do notation which can be of any type. Will have a look at the other suggestion, thanks ! |
Ok, lets consider type HasHandling n m = HasState (HandlingState n) m But the key functions of HandlingState are Btw, a better name for the type class I proposed would be |
I think your
Yes, when we call provideFoo :: HasHandling SomeFixedM SomeFixedM => Cell SomeFixedM a Foo
provideFoo = handling foo But we can use this in a general monad: useFoo :: (HasHandling SomeFixedM m, Has BarT m) => Cell m a b
useFoo = hoistCell liftH provideFoo >>> bar The monad I might be wrong somehow, but I think this can work in principle. Maybe a worked out example would be helpful. |
Hi
Hum, I don't think I follow here.
I don't think I'm completely following. One thing is that in the current formulation of vivid and portmidi there is no fixed SomeFixedM, it can be any monad as long as it is an instance of MonadIO:
If we want to keep the same flexibility than we have to deal with an arbitrary Perhaps a simple example would be mixing an exception and handling together, e.g.: test :: Monad m => Cell (ExceptT () (StateT (HandlingState m) m)) () ()
test = proc () -> do
generalizedHandling (Handle (return ()) (const (return ()))) -< ()
generalizedThrowC -< ()
edit: Actually in the specific case above, generalizedThrowC or current throwC would both work. |
Thanks, for pointing it out. Indeed, I have known about |
Sorry, I was a bit confused. The We can define this: provideFoo :: Cell (HandlingStateT SomeFixedM) a Foo
provideFoo = handling foo But I think it's still true that we can use it in here: useFoo :: (HasHandling SomeFixedM m, Has BarT m) => Cell m a b
useFoo = hoistCell liftH provideFoo >>> bar
Yes, because the portmidi handles are in providePortMidiInput :: MonadIO m => Cell (HandlingStateT (PortMidiT m) a Something`
providePortMidiInput = handling $ portMidiInputStreamHandle "FooDevice"
usePortMidi :: (MonadIO m, MonadIO n, HasHandlingState m n) => Cell n a Foo
usePortmidi = hoistCell liftH providePortmidiInput >>> foo Now It's important to note then that when we want to do
I think this doesn't work in general. At least not without a constraint like |
Thanks for thinking about this with me ! :-)
Ok, so I tried putting the code above in Haskell with a few corrections, and I made the types fully explicit so we can see what is going on. providePortMidiInput :: MonadIO m => Cell (StateT (HandlingState m) m) a (Either EOLCPortMidiError PortMidiInputStream)
providePortMidiInput = handling $ portMidiInputStreamHandle "FooDevice"
usePortMidi :: forall m n a . (MonadIO m, MonadIO n, HasState (HandlingState m) n) => Cell n a (Either EOLCPortMidiError PortMidiInputStream)
usePortMidi = hoistCell liftH (providePortMidiInput @m) It doesn't typecheck with
I'm not sure, but I think that liftH requires that the monad n in t n a -> m a be any type, and not a particular type, and in this case it is fixed to m ?
That is what I thought too, that is why I tried the |
Not sure if this is related to the issue above, but the following gives an error: x1 :: Monad m => StateT (HandlingState m) m ()
x1 = undefined
x2 :: Monad m => ExceptT () (StateT (HandlingState m) m) ()
x2 = liftH x1
I believe liftH cannot resolve which type class instance to use if the type |
Actually, I think the work I had done for my approach to vivid, already solves this issue. The approach I used for MonadState from mtl also works for Has (ST is strict StateT): transformersToHasState :: (HS.HasState s t, MonadBase m t) => ST.StateT s m a -> t a
transformersToHasState m = do
currentState <- HS.get
(a, newState) <- liftBase $ ST.runStateT m currentState
HS.put newState
return a
cellGenerelizeHandlingStateT :: (HS.HasState (HandlingState m) t, MonadBase m t) =>
Cell (HandlingStateT m) a b -> Cell t a b
cellGenerelizeHandlingStateT = hoistCell transformersToHasState
generalizedHandling :: (HS.HasState (HandlingState m) t, MonadBase m t, Typeable h) =>
Handle m h -> Cell t a h
generalizedHandling = cellGenerelizeHandlingStateT . handling I think the key trick is the use of Perhaps I will give it a go at generalizing the code in essence-of-live-coding using this approach. ps1: instance (MonadBase b m) => MonadBase b (PortMidiT m) where liftBase = liftBaseDefault
instance Monad m => Has (ST.StateT (HandlingState m)) (PortMidiT m) where
liftH m = PortMidiT $ liftH m I haven't quite figured out yet why in the instance above I cannot write |
I've opened a PR implementing what we discussed here. |
On reddit discussion about something similar to the IsSublayer approach. Interesting. |
Would it make sense to always use the Has type class in all packages here instead of explicit monad transformer stacks ? It would be a bit of work converting all functions to use
Has
, and type signatures would be a bit more abstract, but on the positive side it would mostly eliminate the use of lifts in eolc-based code, right ?The text was updated successfully, but these errors were encountered: