wiki:Proposal/MonadOfNoReturn

This proposal has been revised based on the feedback gathered from the proposal discussion. The original revision of this proposal can be found here. See wiki page History for changes relative to that first revision.

Monad of no return/>> Proposal (MRP) 2e

Abstract

To complete the Monad-hierarchy refactoring started with AMP (& MFP) and unify return/pure & >>/*>, move Monad(return) and Monad((>>)) methods out of the Monad class into top-level bindings aliasing Applicative(pure) and Applicative((*>)) respectively.

The original proposal didn't include (>>) yet. But in the interest of bundling related changes, taking care of (>>) has been added to this proposal.

Current Situation

With the implementation of Functor-Applicative-Monad Proposal (AMP)[1] and (at some point) the MonadFail proposal (MFP)[2] the AMP class hierarchy becomes

class  Functor f  where
    fmap    :: (a -> b) -> f a -> f b

class  Functor f => Applicative f  where
    pure    :: a -> f a
    (<*>)   :: f (a -> b) -> f a -> f b
 
    (*>)    :: f a -> f b -> f b
    u *> v  = 
 
    (<*)    :: f a -> f b -> f a
    u <* v  = 

class  Applicative m => Monad m  where
    (>>=)   :: m a -> (a -> m b) -> m b

    return  :: a -> m a
    return  =  pure

    (>>)    :: m a -> m b -> m b
    m >> k  =  m >>= \_ -> k

class  Monad m => MonadFail m  where
    fail    :: String -> m a

Consequently, the Monad class is left with a now redundant return method as a historic artifact, as there's no compelling reason to have pure and return implemented differently.

More to the point, this redundancy violates the "making illegal states unrepresentable" idiom: Due to the default implementation of return this redundancy leads to error-prone situations which aren't caught by the compiler; for instance, when return is removed while the Applicative instance is left with a pure = return definition, this leads to a cyclic definition which can be quite tedious to debug as it only manifests at runtime by a hanging process.

Traditionally, return is often used where pure would suffice today, forcing a Monad constraint even if a weaker Applicative would have sufficed. As a result, language extensions like ApplicativeDo[3] have to rewrite return to weaken its Monad m => constraint to Applicative m => in order to benefit existing code at the cost of introducing magic behavior at the type level.

An additional (somewhat minor) benefit results from having smaller class dictionaries, as well as avoiding the additional indirection through the Monad class dictionary (when the class dictionary can't be resolved at compile time).

For (>>), in addition to arguments applying to return, the status quo is optimising (>>) and forgetting about (*>), resulting in unexpected performance regressions when code is generalised from Monad to Applicative. This unfortunate situation also blocks us from being able to remove the post-AMP method redundancy in the Foldable/Traversable classes.

Finally, this redundancy becomes even more significant when viewed in light of the renewed Haskell standardisation process[7]: The next Haskell Report will almost certainly incorporate the AMP (and MFP) changes, and there is no justification for the next Haskell Report to retain return nor (>>) as methods of Monad. A good reason would have been to retain backward compatibility with Haskell 2010. However, as the AMP superclass hierarchy requires Monad instances to be accompanied by Applicative instances (which aren't part of Haskell 2010, c.f. [6]), backward compatibility with Haskell 2010 goes out the window when it comes to defining Monad instances (unless via use of -XCPP or similar). Consequently, meeting the high bar for a formal document such as the Haskell Report demands that Monad shall not carry a redundant return method that serves no purpose anymore. Moreover, getting return out of the way is desirable to facilitate standardising potential candidates such as the earlier mentioned ApplicativeDo in the future and avoids the technical debt incurred by keeping around this language wart.

When considered out of context, the enumerated reason above could be considered weak on their own and would maybe not be enough to carry this proposal individually. But put together, those smaller benefits form one bigger composite benefit, and taken in the context of the recent AMP, and the upcoming MFP, the costs are comparatively low (especially with the reduced-breakage-transition-strategy), and it makes sense to settle this technical debt soon while it's still relatively cheap.

It's easy to underestimate the infinite accrued cost of retaining language warts which persist in the language indefinitely.

Proposed Change

Remove return and (>>) as methods from the Monad class and in its place define top-level bindings with the weaker Applicative typeclass constraint:

-- | Alias for 'pure' 
return :: Applicative f => a -> f a
return = pure

-- | Alias for `(*>)`
(>>) :: Applicative f => f a -> f b -> f b
(>>) = (*>)

This allows existing code using return to benefit from a weaker typeclass constraint as well as cleaning the Monad class from redundant methods in the post-AMP world.

A possible migration strategy is described further below.

Compatibility Considerations

Generalizing the type signature of a function from a Monad constraint to its superclass Applicative doesn't cause new type-errors in existing code.

However, moving a method to a top-level binding obviously breaks code that assumes e.g. return to be a class method. Foremost, code that defines Monad instances is at risk:

Instance Definitions

Code defining return (or (>>)) as part of an instance definition breaks. However, (>>) has a default implementation in Haskell 98/2010 in terms of (>>=), and we had the foresight to provide a default implementation in base-4.8 for return so that the following represents a proper minimal instance definition post-AMP:

instance Functor Foo where
    fmap g foo  = 

instance Applicative Foo where
    pure x      = 
    a1 <*> a2   = 

instance Monad Foo where
    m >>= f     = 

    -- NB: No mention of `return` nor `(>>)`

Consequently, it is possible to write forward-compatible instances omitting return that are valid starting with GHC 7.10/base-4.8.

Heuristically greping through Hackage source-code reveals a non-negligible number of packages defining Monad instances with explicit return definitions[4]. This has a comparable impact to the AMP, and similarly will require a transition scheme aided by compiler warnings.

As large code bases are reported to not have been updated to GHC 7.10 yet, it's more economical to follow-up with MRP warnings closely in the wake of AMP/MFP to have all required Monad-related changes applied in one sweep when upgrading.

Module Import/Export Specifications

A second source of incompatibility may be due to imports. Specifically module import that assert return to be a method of Monad, e.g.:

import Control.Monad  (Monad ((>>=), return))

or

import Prelude hiding (Monad(..))
import Control.Monad  (Monad(..)) as Monad

f = Monad.return ()

The dual situation can occur when re-exporting return or (>>) via module export specifications.

However, given that return and (>>) are (re)exported by Prelude and the examples above are rather artificial, we don't expect this to be a major source of breakage in the case of return. In fact, a heuristic grep[5] over Hackage source-code revealed only 21 packages affected.

Tool for (Semi)Automatic Refactoring

There is ongoing work in the Hs2010To201x project to provide automatic refactoring assistance for migrating pre-AMP code-bases to AMP+MFP+MRP. Tooling of this sort can dramatically reduce the maintenance cost incurred by the recent Monad restructuring changes.

Example for writing future-proof code

GHC extension to reduce code-breakage:

When (>>) and return are moved out of the Monad class, GHC would still tolerate (as a NO-OP) the lawful definitions for (>>) and return as used in the example above (and otherwise emit an error).

This way, code can be made forward compatible the desired semantics without the use of -XCPP.

instance Functor Foo where
    fmap f x    = 

instance Applicative Foo where
    pure x      = 
    a1 <*> a2   = 
    a1 *>  a2   =   -- only needed when
                     -- optimised version possible

instance Monad Foo where
    m >>= f     = 

    -- see note for GHC extension ignoring the two 
    -- lawful definitions:
    (>>)   = (*>)
    return = pure  -- only needed for compatibility
                   -- with base < 4.8

Migration Strategy

Original Simple Variant

This transition scheme is not proposed anymore; see new strategy below

In this transition scheme, the time when Phase 2 starts is determined by the amount of packages already converted at that time. "GHC 8.2" is only the earliest theoretical time to begin Phase 2, but a more realistic time would be "GHC 8.6" or even later.

The migration strategy is straightforward:

Phase 1 (GHC 8.0)
Implement new warning in GHC which gets triggered when Monad instances explicitly override the default return/(>>) methods implementation.
Phase 2 (GHC 8.2 OR LATER)
When we're confident that the majority of Hackage has reacted to the warning (with the help of Stackage actively pursuing maintainers to update their packages) we turn the return and (>>) methods into a top-level binding and remove the warning implemented in Phase 1 from GHC again.

Reduced Breakage Variant

Based on the feedback from the proposal discussion this revised transition scheme is expected to address all concerns raised: This scheme aims to avoid breakage while allowing code to be written in a way working across a large range of GHC versions with the same semantics. Specifically, this allows a 3-year-compatibility window, avoidance of -XCPP, as well as the ability to write code in such a way to avoid any warnings.

Phase 1 (starting with GHC 8.0)
Implement new warning in GHC which gets triggered when Monad instances explicitly override the default return and (>>) method implementations with non-lawful definitions (see compatible instance definition example in previous section).

This warning can be controlled via the new flag -fwarn-mrp-compat, and becomes part of the -Wall and -Wcompat (#11000) warning-sets, but not part of the default warning-set.

Phase 2 (GHC 8.4 or even later)
When we're confident that the majority of Hackage has reacted to the warning (with the help of Stackage actively pursuing maintainers to update their packages) we turn the return and (>>) methods into a top-level binding and let GHC ignore lawful method definitions of return and (>>).

Non-lawful definitions (which were warned about in Phase 1) will now result in a compile error, while lawful definitions will be ignored and not be warned about (not even with -Wall).

Phase 3 (very distant future)
Start warning about lawful return/>> method overrides (in order to prepare for Phase 4)
Phase 4 (even more distant future)
Remove support in GHC for ignoring lawful return/>> overrides, turning any method override of return and (>>) into a compile error.

Discussion

Discussion Period

A discussion period of three weeks (until 2015-10-15) should be enough to allow everyone to chime in as well as leave enough time to make the required preparations for GHC 8.0 should this proposal pass as we hope.

Discussion Summary

See MonadOfNoReturn/Discussion


Last modified 2 years ago Last modified on Nov 25, 2015 12:20:40 PM