Skip to content
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

Add MFunctor instances #28

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

turion
Copy link
Contributor

@turion turion commented Jul 12, 2021

The idea behind MFunctor is that ProgramT and ProgramViewT should be compatible with functions like lift :: MonadTrans t => m x -> t m x, liftIO :: MonadIO m => IO x -> m x, flip evalStateT s :: StateT s m a -> m a, flip runReaderT r :: ReaderT r m a -> m a, and so on. Such functions are "monad morphisms", and hoist allows to use them on the inner monad of an operational construction.

For example, hoist liftIO :: MonadIO m => ProgramT instr IO a -> ProgramT instr m a.

Another typical use case is when you want to handle some part of your monad stack inside ProgramT, i.e. hoist $ flip runReaderT r :: ProgramT (ReaderT r m) a -> ProgramT m a.

I often need functions like this.

To do

  • Haddocks

@turion
Copy link
Contributor Author

turion commented Jul 12, 2021

If you think this is valuable, I'll go on documenting. Maybe an example in the module doc? And some comments on the instances?

@turion
Copy link
Contributor Author

turion commented Sep 6, 2023

@HeinrichApfelmus ping :) any interest? Are docs needed?

@HeinrichApfelmus
Copy link
Owner

HeinrichApfelmus commented Jun 6, 2024

Thanks for pinging me! I finally managed to take a look, specifically concerning the question of whether this function requires access to the constructors or not. 🤔

Wow. 😲 It looks like the definition of MFunctor is wrong — it does not include a constraint that the target of the natural transformation, n, is also a monad.

If the target were a monad, we could write the following function in terms of viewT

hoistProgramT
    :: (Monad m, Monad n)
    => (forall a. m a -> n a)
    -> ProgramT instr m b
    -> ProgramT instr n b
hoistProgramT nat program =
    joinProgramT $ nat $ do
        firstInstruction <- viewT program
        return $ case firstInstruction of
            Return a -> return a
            i :>>= k -> singleton i >>= (hoistProgramT nat . k)

joinProgramT :: Monad n => n (ProgramT instr n a) -> ProgramT instr n a
joinProgramT = join . lift

and then define

instance MFunctor (ProgramT instr) where
   hoist = hoistProgramT

However, the Haskell compiler will reject this definition of hoist as hoistProgramT requires a Monad n constraint that is missing from hoist. In a sense, the hoist function from the mmorph package is too general — it wants to work with any target type constructor n, regardless of whether that type is a monad or not.

The reason why you are able to implement it with direct access to the constructors is that the constructor Lift does not include the Monad constraint, while its smart version lift does:

Lift :: m a -> ProgramT instr m a
-- vs
lift :: Monad m => m a -> ProgramT instr m a

Hm. Given that I think that the definition of hoist in the mmorph package is wrong (too general), I'm no longer sure that I want to depend on mmorph in the first place. 🤔

@turion
Copy link
Contributor Author

turion commented Jun 6, 2024

Thanks for coming back to this! :)

It looks like the definition of MFunctor is wrong — it does not include a constraint that the target of the natural transformation, n, is also a monad.

I don't quite agree with your assessment. See Gabriella439/Haskell-MMorph-Library#11, people have suggested to add the additional constraint before, and it wasn't needed.

I've written quite a few hoist instances, and in fact I've never needed the additional constraint after thinking about long enough, and having the constructors available. In fact, ProgramT is mentioned as a counterexample only because the constructors are hidden! Gabriella439/Haskell-MMorph-Library#11 (comment)
Adding the additional constraint, however, would break some existing instances, so it's unlikely to come upstream any time soon.

So in summary I believe that:

  • The missing constraint is a conscious & good design decision, it is not wrong per se
  • MFunctor can and should be implemented using the direct constructors and not the "smart" constructors (those are for library users, not implementors)

@HeinrichApfelmus
Copy link
Owner

  • MFunctor can and should be implemented using the direct constructors and not the "smart" constructors (those are for library users, not implementors)

I'm afraid, but I disagree on this point. 😅

ProgramT is an abstract data type — the public interface is a sound and complete characterization of what it can do. If the desired functionality cannot be implemented in terms of the public interface, this functionality doesn't exist. This means that a function with type signature

hoistProgramT
    :: (Monad m, Monad n)
    => (forall a. m a -> n a)
    -> ProgramT instr m b
    -> ProgramT instr n b

exists, and that a function with type signature

hoist
    :: Monad m
    => (forall a. m a -> n a)
    -> ProgramT instr m b
    -> ProgramT instr n b

most likely does not exist.

I'm happy to add an implementation hoistProgramT in terms of the original constructors for better performance, but I don't want to break the abstraction barrier of not having the constructors available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants