haskell / core-libraries-committee

95 stars 15 forks source link

Add quantified superclass to Bifunctor #91

Closed Icelandjack closed 1 year ago

Icelandjack commented 2 years ago

Add a quantified constraint forall a. Functor (bi a) to connect Bifunctor to Functor. The superclass logically follows from the meaning of Bifunctor and Edward Kmett has done the same for Profunctor in his own library.

{-# Language QuantifiedConstraints #-}

class (forall a. Functor (bi a)) => Bifunctor bi where
  ..

Here is a comment from Kmett about how Profunctor is waiting on this change in Bifunctor:

Once we have the superclass for Bifunctor we can fix the Cayley/Tannen split, which can then become one type.

I stalled a bit in actually doing this on my own primarily because Bifunctor moved into base and outside of my ability to unilaterally make the change. (Which means the bifunctors package is currently stuck because I can't line it up with the profunctors package HEAD until that superclass is in place.)

Related issues

Topsii commented 1 year ago

One can construe an example that shows why fmap = bimap id is dangerous. The example is similar to my previous comment, which shows why fmap = second is dangerous. Suppose someone had previously defined their instances like this:

instance Functor CustomDataType where
  fmap = bimap id

instance Bifunctor CustomDataType where
  first = actual implementation
  second = actual implementation

If second is removed from the minimal complete set, the default definition will change to bimap f g = first f . fmap g. Then any call of either fmap or bimap would loop.

A noncanonical-bifunctor-instances warning would warn about the backwards definitions fmap = second and fmap = bimap id similar to how noncanonical-monoid-instances and noncanonical-monad-instances already warn us today about the backwards definitions (<>) = mappend, pure = return and (*>) = (>>).

If we want to remove second from the minimal complete set, then I believe the migration guide should suggest to give a custom definition for fmap. One easy way is to copy the definition of second to define fmap and subsequently remove the definition of second or change it to second = fmap. Alternatively if second is not defined, one can instead copy the definition of bimap f g = ... and simplify it by substituting id for f. The Functor instance can often be derived too.

Note the previously estimated extent of the breakage if we plan to remove second from the minimal complete set.

For reference there are 416 nonderived Bifunctor instances on Hackage.

Ericson2314 commented 1 year ago

@Topsii what confuses me is that today's warnings are about the redundant method is the downstream class. Analogously. they would care about something second = ... other than second = fmap, but they wouldn't care how fmap is defined.

Ericson2314 commented 1 year ago

I think it should be perfect allowable to just define the maximally downstream classes one wishes too, and derive all the upstream class impls, so fmap = bimap id or sconcat = ... mconcat .... should be fine right?

Topsii commented 1 year ago

@Topsii what confuses me is that today's warnings are about the redundant method is the downstream class. Analogously. they would care about something second = ... other than second = fmap, but they wouldn't care how fmap is defined.

The documentation says they care about both classes. For example:

Warn if noncanonical Applicative or Monad instances declarations are detected.

When this warning is enabled, the following conditions are verified:

In Monad instances declarations warn if any of the following conditions does not hold:

    If return is defined it must be canonical (i.e. return = pure).
    If (>>) is defined it must be canonical (i.e. (>>) = (*>)).

Moreover, in Applicative instance declarations:

    Warn if pure is defined backwards (i.e. pure = return).
    Warn if (*>) is defined backwards (i.e. (*>) = (>>)).

This states that noncanonical-monad-instances warns about code in both Monad and Applicative instances.

I think it should be perfect allowable to just define the maximally downstream classes one wishes too, and derive all the upstream class impls, so fmap = bimap id or sconcat = ... mconcat .... should be fine right?

This contradicts the concept of canonicity, as hvr writes:

starting with base-4.8, the return method gained a default implementation return = pure which follows the preferred or "canonical" direction of having implementations flow from superclasses to their subclasses.

In the case of Bifunctor we want to remove second from the minimal complete set:

  class (forall a. Functor (p a)) => Bifunctor p where
-     {-# MINIMAL bimap | first, second #-}
+     {-# MINIMAL bimap | first #-}

      bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
-     bimap f g = first f . second g
+     bimap f g = first f . fmap g

      first :: (a -> b) -> p a c -> p b c
      first f = bimap f id

      second :: (b -> c) -> p a b -> p a c
-     second = bimap id
+     second = fmap

Obviously we cannot have both second = fmap and fmap = second, because that would loop. Canonicity ensures that fmap is not defined in terms of second or bimap such that we can remove second from the minimal complete set without worrying about such loops.

Bodigrim commented 1 year ago

Adding a GHC warning for non-canonical instances is quite a big deal. To implement it Bifunctor has to become a magical type class, implemented in ghc-prim, not in base.

We are in early days of using QuantifiedConstraints for type class hierarchy. I’d wait until this proposal is battle-tested and the destiny of proposals for Bifoldable / Bitraversable / Profunctor is determined, before making a GHC proposal.

For migration guide I’d recommend users to move their definition of second to fmap and redefine second = fmap afterwards.

Bodigrim commented 1 year ago

Most of the time instance Functor can be derived, no need to implement fmap manually.

Ericson2314 commented 1 year ago

@Topsii I think those warnings on "defined backwards" are just for when the redundant method is used verbatim. So second = fmap would fall under that, but second = bimap id would not.

I think the purpose is merely so if the default definition of second is changed, code won't suddenly start diverging.

chshersh commented 1 year ago

I'm trying to summarise the state of this proposal as part of my volunteering effort to track the progress of all approved CLC proposals.

Field Value
Authors @Topsii, @Icelandjack
Status merged
base version 4.18.0.0
Merge Request (MR) https://gitlab.haskell.org/ghc/ghc/-/merge_requests/9313
Blocked by nothing
CHANGELOG entry present
Migration guide https://github.com/haskell/core-libraries-committee/blob/main/guides/bifunctor-superclass.md

Please, let me know if you find any mistakes 🙂

hasufell commented 1 year ago

The build errors all relate to missing Functor instances. Some of them also had Bifoldable and Bitraversable instances but no Foldable and Traversable instances.

It seems our impact assessments assume that patches will be merged. What if they won't, e.g. because it was a conscious decision to omit an instance.

https://github.com/haskell-foundation/foundation/pull/572

We should be more careful with things that impact alternative preludes.

Bodigrim commented 1 year ago

(Vincent banned me long ago, so let me comment here)

It seems our impact assessments assume that patches will be merged. What if they won't, e.g. because it was a conscious decision to omit an instance.

As a matter of principle: we cannot allow a single maintainer, unwilling to accept simple patches, to block changes, which we otherwise find desirable. foundation has long been known as a liability and a laggard to support new GHCs, even despite community's efforts to provide necessary patches promptly. I've seen this drama with GHC 9.0, and 9.2, and 9.4, and by 9.6 it grew too old. People should have migrated away long ago.

On this specific instance: I know that some people still find instance Foldable (a,) controversial because of length behaviour (although we are almost a decade past this choice, go use Hugs if you wish). But there is no controversy with instance Functor (a,) and no reason not to define it.

Bodigrim commented 1 year ago

For the sake of any new readers, I should also mention that there've been plenty of time to raise concerns about this proposal. The discussion has started in September 2022, the PR for foundation was raised a month later, in October, giving maintainers an explicit opportunity to voice an opinion, and finally the vote has been held in the late November. But if one kept silence all this time, it's unreasonable to get angry about the change half a year later.

I've seen this pattern before in https://github.com/haskell/core-libraries-committee/issues/3#issuecomment-968286173, and it makes me sad. If you are unhappy with a proposal, please do chime in, have your say here, not just vent on Twitter or gnash teeth silently.


Note that foundation is not consistent about not providing Functor instances for types with two independent parameters: e. g., These a b has instance Functor (These a). Judging from this, not having instance Functor (Tuple a) looks an inadverent omission, not a general principle or belief.

Ericson2314 commented 1 year ago

Very much agree with both of those posts.

And for what it is worth Bifunctor is exactly the sort of thing I would like to see decoupled from GHC, so we can continue improving our libraries and not breaking any stick-in-the-muds or laggards. The best of both worlds.

vincenthz commented 1 year ago

For the sake of any new readers, I should also mention that there've been plenty of time to raise concerns about this proposal. The discussion has started in September 2022, the PR for foundation was raised a month later, in October, giving maintainers an explicit opportunity to voice an opinion, and finally the vote has been held in the late November. But if one kept silence all this time, it's unreasonable to get angry about the change half a year later.

Wow. between a whole month and two months to react, how gracious.

The lack of shipping those potential changes in alpha/beta forms to users, means maintainers need to keep on top of the bleeding edge development (and PR if they are lucky) until it's pushed as 'stable' to the unsuspecting users as a new base release.

It's funny how some other languages have understood this balance very well, and enjoy way more successes in the real world ... but I guess I'm the one 'unreasonable'

Note that foundation is not consistent about not providing Functor instances for types with two independent parameters: e. g., These a b has instance Functor (These a). Judging from this, not having instance Functor (Tuple a) looks an inadverent omission, not a general principle or belief.

you're wrong, simple overzealous mirroring of the existing datatype from the 'these' package. Funny how you can 'judge' things without knowledge for your benefits when that suits you.

tomjaguarpaw commented 1 year ago

lack of shipping those potential changes in alpha/beta forms

By the way, I believe this is now available through GHC nightlies. See, for example, https://www.haskell.org/ghcup/guide/#nightlies