Closed Icelandjack closed 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.
fmap
:
There are 18 noncanonical backwards definitions on Hackage. Specifically there are 16 occurences of fmap = second
and two occurences of fmap = bimap id
. Somewhat tragically I wrote one of the two, when I was creating patches for this proposal.second
:
There are 52 instances on Hackage that define second
but do not define it as second = fmap
. The simple fix is to change them to second = fmap
.For reference there are 416 nonderived Bifunctor instances on Hackage.
@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.
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 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 thansecond = fmap
, but they wouldn't care howfmap
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
orsconcat = ... 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.
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.
Most of the time instance Functor
can be derived, no need to implement fmap
manually.
@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.
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 🙂
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.
(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.
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.
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.
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 providingFunctor
instances for types with two independent parameters: e. g.,These a b
hasinstance Functor (These a)
. Judging from this, not havinginstance 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.
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
Add a quantified constraint
forall a. Functor (bi a)
to connectBifunctor
toFunctor
. The superclass logically follows from the meaning ofBifunctor
and Edward Kmett has done the same forProfunctor
in his own library.Here is a comment from Kmett about how
Profunctor
is waiting on this change inBifunctor
:Related issues
92 Add
Profunctor
with a quantified superclass93 Add quantified superclass to
Bifoldable
andBitraversable