-
Notifications
You must be signed in to change notification settings - Fork 16
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
Proposal: Remove method (/=) from class Eq #3
Comments
I'm -1 on this change. I've >10 years experience teaching Haskell and its never been an issue with teaching. |
I'm +1. This is a fantastic simplifying change. There's no reason not to do this, other than extremely trivial breakages. And I would really like to live in a world where we don't let the burden of already-written code get in the way of turning Haskell into the language we want it to be. |
this breaks code for no reason. Lets not touch this till we have better support for warning about redundantly defined type class methods such as having a "XOR" warning minimal pragma change such as I suggest in https://mail.haskell.org/pipermail/libraries/2021-October/031490.html |
I'm +1 on this. As it stands, Haskell's type class system does not have a framework for "class laws": at best, they are guidelines for how types fitting that class should behave. There is no way within a class' definition to guarantee that only those types with lawful implementations should be bestowed the class. The burden lies on the library designer to help guide their users away from footguns; unlawful instances are some of those. By removing Also, who really writes Thanks for raising this issue, @nomeata! |
To facilitate internal CLC discussions, I’d like to gather some additional input:
|
Concrete example: In a tutorial of my own I explain the concept of default methods when I introduce the |
I'd probably flip a coin
Hutton's Programmin gin Haskell says in 3.9 basic classes section Eq - equality types ... using following two methods (==) :: a -> a -> Bool
(/=) :: a -> a -> Bool However, the presentation in that chapter just tells these functions I don't think that would trap future readers of an old edition book who use newer GHC
See below. Though I'm not proponent nor opponent of this change, I just want
I'd accept a PR, though I'd just do it myself during the usual new-GHC-support I built Stackage LTS-18.8 set (which I had around for other experiment).
(and Most changes are oneliners. Either removing explicit I can upload patches somewhere, if anyone is interested to take a closer look Few packages are more interesting: clash-prelude This is interesting application. The alternative or special purpose preludes:
GHC (like) code: complicated /= example https://hackage.haskell.org/package/numbers data Interval a = I a a
ival :: (Ord a) => a -> a -> Interval a
ival l h | l <= h = I l h
| otherwise = error "Interval.ival: low > high"
instance (Ord a) => Eq (Interval a) where
I l h == I l' h' = l == h' && h == l'
I l h /= I l' h' = h < l' || h' < l https://hackage.haskell.org/package/DBFunctor package has: -- | Definition of the Relational Data Type. This is the data type of the values stored in each 'RTable'.
-- This is a strict data type, meaning whenever we evaluate a value of type 'RDataType',
-- there must be also evaluated all the fields it contains.
data RDataType =
RInt { rint :: !Integer }
| RText { rtext :: !T.Text }
| RUTCTime { rutct :: !UTCTime}
| RDate {
rdate :: !T.Text
,dtformat :: !Text -- ^ e.g., "DD\/MM\/YYYY"
}
| RTime { rtime :: !RTimestamp }
| RDouble { rdouble :: !Double }
-- RFloat { rfloat :: Float }
| Null
-- | We need to explicitly specify equation of RDataType due to SQL NULL logic (i.e., anything compared to NULL returns false):
-- @
-- Null == _ = False,
-- _ == Null = False,
-- Null /= _ = False,
-- _ /= Null = False.
-- @
-- IMPORTANT NOTE:
-- Of course this means that anywhere in your code where you have something like this:
-- @
-- x == Null or x /= Null,
-- @
-- will always return False and thus it is futile to do this comparison.
-- You have to use the is 'isNull' function instead.
--
instance Eq RDataType where
RInt i1 == RInt i2 = i1 == i2
-- RInt i == _ = False
RText t1 == RText t2 = t1 == t2
-- RText t1 == _ = False
RDate t1 s1 == RDate t2 s2 = toRTimestamp (unpack s1) (unpack t1) == toRTimestamp (unpack s2) (unpack t2) -- (t1 == t1) && (s1 == s2)
-- RDate t1 s1 == _ = False
RTime t1 == RTime t2 = t1 == t2
-- RTime t1 == _ = False
RDouble d1 == RDouble d2 = d1 == d2
-- RDouble d1 == _ = False
-- Watch out: NULL logic (anything compared to NULL returns false)
Null == Null = False
_ == Null = False
Null == _ = False
-- anything else is just False
_ == _ = False
Null /= Null = False
_ /= Null = False
Null /= _ = False
x /= y = not (x == y) I.e. it's a DSL trying to emulate SQL-like processing in Haskell. However, when removing the
|
It would be good to get away from the blatantly unlawful instance in Exactly the same code appears in |
The poorness there is try to model 3VL with two-valued https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL) |
In terms of performance I would expect for real world code:
* A very very small compile time benefit most of the time.
* No runtime difference for most runtime relevant use cases.
* A small to moderate improvement in runtime (not allocation) in some
edge cases.
In particular it should only make a difference for code which is not
specialized
*and***uses the class method lazily or uses both methods.
So from a performance PoV, if we were to design it today there would be
a reason to have it be a single method.
But it's unclear if it's worth breaking packages now for the small
benefit it provides.
Am 27/10/2021 um 19:45 schrieb Bodigrim:
…
To facilitate internal CLC discussions, I’d like to gather some
additional input:
1. If we were to design Haskell from scratch today, would we prefer
one or two members of |Eq|? Are there arguments to prefer two today?
2. Does this change affect educational materials or text books? Could
anyone bring specific examples?
3. Are proponents of the change willing to facilitate ecosystem’s
migration by raising PRs to affected libraries? Are maintainers
willing to accept such PRs? CC @phadej <https://github.com/phadej>
for |postgresql-simple|, @paul-rouse
<https://github.com/paul-rouse> for |mysql-simple|, @treeowl
<https://github.com/treeowl> for |containers|.
4. What is an impact of this change on GHC and compilation times? CC
@AndreasPK <https://github.com/AndreasPK>.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#3 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABJ65ZWUO6ZWCLDJSM4IMD3UJBCGJANCNFSM5GZ2YRQQ>.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
CC @christiaanb for |
Using type class backends we could transparently evolve |
Something like
|
I find it hard to form a particularly strong opinion on this either way. It seems like a fairly minor breakage for very minor wins. Some thoughts: Right now I'm a very weak -1 on this. |
As a Haskell user I probably wouldn’t notice this (I always derive Really, why is your proposal to remove To summarise, I’m leaning slightly against this proposal, but wouldn’t lose my sleep over it. |
Does the same reasoning apply to |
Nope. As the docs to that say: " The default definition is fmap . const, but this may be overridden with a more efficient version." The argument for removing |
When we add a type class member, which can be expressed via others, there is only one possible excuse to do so: one can provide a more efficient implementation than default. Since this is not the case for Given that the migration path is straightforward and backward compatible and that maintainers of SQL libraries above are on board with cleaning infelicities in |
This is speculation. @nomeata's test indicate that it's not an issue. (Compiling Cabal, see the mailing post https://mail.haskell.org/pipermail/libraries/2021-October/031484.html). @nomeata have you already run |
Many people with good judgement believe there is some merit to this proposal. I confess I cannot see what they see. I am very much against churn for the sake of minor improvement. The Haskell community already has a reputation of insufficiently valuing stability. On the other hand, perhaps this change will have minimal impact. Indeed the only way I could countenance it is if there is no impact on maintainers. That seems to imply that someone must take responsibility for ensuring that the entirety of Hackage builds with this change before the change is actually applied. If that happens then I won't oppose the proposal (not that my voice carries any weight) but I still can't understand the motivation. |
No, I did not. I shy away from performance measurements because it's so hard to get them meaningfully right, at least for runtime changes in sub percentage space. |
@nomeata check
|
I've seen a number of proposals on and off like this recently. I think the reason is likely that there's a an energy to "fixing" Haskell, which I think is a great symptom and energy in the community. However, I advocate against this change, not because the change is bad in isolation, but because accepting this proposal would be symptomatic of a poor and inconsiderate design process, and a lack of unifying philosophy in the CLC. My understanding is that accepting such a proposal would mean that the CLC is saying "we will accept any improvement to the Haskell core libraries that has a minor cost and is an improvement on the past situation." For example, we can take a similar proposal here from @isovector which aimed to export But let's assume we accept this change. Now I come along and look at Ord, and I say "huh", why include anything other than
Note also that the change to The point being, we shouldn't accept that these proposals would each require more files to have In sum, such a change cannot and should not be an isolated change. The real proposal as I see it is "Remove |
@santiweight's comment is in line with my recent thoughts here. There are lots of really good changes we should make to base, all of which keep getting shot down for exactly the same reason of "too much churn." But that doesn't mean we shouldn't do them! It just means we should pick some particular time/release and put all of our big breaking changes into that basket. ofc a better solution would be to unprivilege |
But it is not*. The proposal says
And as other have pointed out, it's difficult to figure out where The examples brought by @effectfully are all with a disclaimer: "(I don't really use the Eq class there)". |
PVP is semantic versioning scheme. There are problems:
Otherwise I agree, It would greatly reduce the compatibility burden. it would be enough to make a version of But that all just wishes. If you don't like the churn, please push for solving the underlying problems, not blocking changes in the mean time. |
@phadej I don't I made my point totally clear in that regard, the point being that the removing |
I would like some large scale coordination on how to make decoupled base a reality. Otherwise we get these massive threads with no easy answers and the CLC can only easily do things at the margins. I think we can do it today with CPP, and then we will want to invest in things like better orphans and better backpack to make the interfaces cleaner. |
I am pushing for the underlying problems! I am currently trying to come up with a non-text-based pre-processor/conditional-compilation extension that would allow a migration tool to be written for changes to namespace etc. I posted in the slack channel yesterday in fact and (once some current personal life stuff is done) I will try to get some implementation over Christmas. But the point also stands that this proposal's acceptance is a worrying and a real_world slippery slope (slippery slopes are valid arguments if they happen), and I am personally not trying to "block changes" but instead to protect users, which is, after-all the whole point of the core libraries. The core principle here is not "make states unrepresentable, but make users happy and productive - the second principle implies the first only some of the time, for the reasons outlined above and in the proposal |
No they won't. {-# OPTIONS -ddump-deriv #-}
module MyMaybe where
data MyMaybe a = MyNothing | MyJust a
deriving ( Eq, Ord )
Note: there is no |
That would seem to be the consistent way to handle this |
I think a clear statement in the Haskell Report on how class laws are supposed to be interpreted would be useful. |
I agree: if the sentiment is that law-breaking instances are not just an unavoidable possibility that falls under “just don't do that, and if you do, you get to keep both pieces” (a bit like orphan instances), but rather something we want to support developers in doing, then I may not have proposed this (because then there is a justification for having |
I am all in favor of removing the bogus floating point representation instances. |
Probably worth having had that discussion before this proposal was accepted (or maybe even before it was proposed). It's much easier to get community buy-in on a change that adheres to already agreed and understood principles! It's much harder to get community buy-in when one is implicitly trying to establish principles during and after discussion on a specific proposal! |
Absolutely! But sometimes you first have to discover that some principles are not universally held… |
I think the fact that |
@augustss, why would that be a shame? Floating point representations have a completely different mathematical structure than most things traditionally considered numbers. |
@treeowl One of the major reasons to introduce type classes in Haskell was to be able to write |
I notice Num does not have a superclass constraint on Eq and Num is
documented as "customarily".
Though I will say I've almost abandoned Num for being too random in its
effect to write working code abstracted over. Num's lack of predictable
behavior has sunk most of my attempts to actually use it. Even for
Integer-like instances.
…On Mon, Nov 22, 2021 at 12:36 PM Lennart Augustsson < ***@***.***> wrote:
@treeowl <https://github.com/treeowl> One of the major reasons to
introduce type classes in Haskell was to be able to write + both for
integers and floating point numbers. This what people expect, and I think
this is what Haskell should allow.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#3 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABZLS3WGZZQN4R3CTOUDN3UNJ5TDANCNFSM5GZ2YRQQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
I think would be better to separate the lawful instances from the "raw instances". Alternatively, have |
This is effectively what Rust does:
While not perfect from a theoretical POV, I think it is in a very nice "sweet spot" between theory and practice. This would have been a step into the right direction for Haskell, not some random shuffling of methods/functions for no good reason. |
Classes with only laws make it hard to read code. Every time you see a method, your have to figure out:
This sounds awful. An alternative is to copy all the methods, so we have |
I'm sorry, I don't understand at all why a class with only laws would make code harder to read. The code could look exactly like it does today, and very little would change when looking up methods. |
@augustss, suppose I'm reading some code and I come across an expression |
-1 for the reasons given by others. This proposal is missing a clear design principle. With the same argument we could eliminate |
At one point I was supportive of the idea of offering lawless and lawful
versions of various classes, at least in my own work, but I've since
recanted by and large, at least within the confines of Haskell.
The two main reasons I generally think adopting LawlessFoo and Foo splits
is a bad idea:
Consider something like liftA3:
liftA3 f m n o = f <$> m <*> n <*> o
liftA3 f m n o = liftA2 f m n <*> o
liftA3 f m n o = liftA2 id (liftA2 f m n) o
liftA3 f m n o = pure f <*> m <*> n <*> o
liftA3 f m n o = (\(a,b,c) -> f a b c) <$> ((,,) <$> m <*> n <*> o)
...
If I don't have any laws I have no reason why I get to know these should
compute the same answer! *Therefore I'm forced to basically supply all of
them to the user and give them all names.* This leads to a combinatorial
explosion in the number of names for operations that are all uselessly
equivalent-except-for-niggling-laws, exposing internal implementation
details we normally use the laws to omit transmitting to the user. Users
now need to know every single detail of every single function they call in
order to function in the ecosystem.
In addition to implementation explosion, type inference will happily supply
the LawlessFoo constraint, even if canonicity of the implementation is
lost, or if the correctness of the operation really does requires all those
laws you dropped on the floor. You can get them back by duplicating all the
operations, by making lawful (+) delegate to the lawless (+~) or something,
but this method doesn't scale as you add finer and finer grained structure
to your class hierarchy to capture the individual weakenings of more and
more laws.
…-Edward
On Mon, Nov 22, 2021 at 8:18 PM David Feuer ***@***.***> wrote:
@augustss <https://github.com/augustss>, suppose I'm reading some code
and I come across an expression 3 * 1_000_000 * 7. Does that mean the
same thing as 21 * 10^6? Maybe. Maybe not. A wild Num instance could
define fromInteger and (*) as utterly arbitrary functions. Another
example: if I see fmap f (fmap g x), can I change that to fmap (f . g) x?
Or do I have to first perform a careful investigation of whether fmap for
this type deletes some of the line breaks? My ability to understand code
*locally* goes out the window.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#3 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKMEOPKXNCB42XIBRS5WTUNLTXBANCNFSM5GZ2YRQQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
This is the main thing for me. When correctness because the enemy of automation. correctness looses. (In this case, the automation in question is "type tetris", semi-consciously changing things just thinking about the error messages and not what meaning of what one is writing) For NaN, I honestly think the right thing to do is teach GHC that:
Done right, there should be no performance cost to this, at least one one "overuses" the |
In other words won't be implemented in foreseeable future. Thanks. EDIT: https://github.com/haskell/core-libraries-committee/blob/main/guides/no-noneq-in-eq.md is outdated. It speaks about GHC-9.6, but as there is no progress on either of GHC issues, I'd not promise any GHC version. |
Indeed progress has stalled on some internals of GHC here, in a way. In addition, the manurestorm that this proposal caused made it tempting to push this through. 🤷♂️ |
I'm trying to summarise the state of this proposal as part of my volunteering effort to track the progress of all
Please, let me know if you find any mistakes 🙂 @nomeata could you share a status update on the implementation and what are next steps if anything changed since Dec 2022? Also, please do let CLC know if you need any help, so we can coordinate and prioritise approved proposals accordingly! |
Thanks for the nudge! Nothing new really. To make this change with the least possible disruption, GHC first has to stop treating single-method classes different from multi-method classes (https://gitlab.haskell.org/ghc/ghc/-/issues/20897). The whole thing got entangled in a confusing mess including how to implement Eventually I ran out of steam, and the partial work now just sits there, waiting for someone to pick it up. These changes to GHC are likely desirable even without the Eq-of-no-neq proposal, but it’s not fully clear that they are good. I was a bit burned by the outcry after the Eq-of-no-neq proposal and am not going to push for it. So basically there is some low priority work on the GHC side blocking this, so this proposal will likely just have to sit until that is done. |
As a historical note, with this proposal we made a full circle back to Haskell Report 1988 which defines class Eq a where
(==) :: a -> a -> Bool
class Eq a => Ord a where
(<), (<=) :: a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool
(>), (>=) :: Ord a => a -> a -> Bool
max, min :: Ord a => a -> a -> a |
@Bodigrim suggested I take the proposal that I recently submitted to
libraries@
here, to play guinea pig for this process.What
I suggest to change
to
and turn
(/=)
into a normal function.Why
If we’d define
class Eq
now, we wouldn’t include(/=)
.In general, the motivations for “redundant” methods with default implementation could be
Here, neither of these apply (justification below), and “fixing” this is worth it, for the following reasons:
Teaching: The
Eq
class is often the first, or one of the first, type classes that beginners will face. This means that educators have to explain what default methods are and why they exist, and then have to apologetically say “but this doesn't really apply here, sorry”. (TheOrd
class is much better in that regard, for example(<=)
can be implemented more efficiently thancompare
.)Development cost and effort: Every developer implementing an
Eq
instance will have to make a decision whether to instantiate(/=)
. They have more documentation to read. And precisely because(/=)
doesn’t really make sense here, they might have to think extra long about whether to instantiate(/=)
or not.Removing the method will save some time of future developers.
Similarly. whenever someone reads code and comes across an
Eq
instance with(/=)
defined explicitly they have extra work to do, and think “Why was this defined?”, “Is it lawful?”. Removing the method from the class alltogether will mean library code gets simpler and easier to read.Removing the method will cause libraries to have less code, not more.
Lawfulness.
Eq
saysx /= y = not (x == y)
, but that is only true if everybody plays by the rules. By having(/=)
it is possible for instances to be unlawful (intentionally or accidentally), for little gain.Removing the method will guarantee that equation.
Code changes are backward compatible. This change will require some library maintainers to change their code. But the code change is simple (remove the method, possibly adjust the
import
statement), and is compatible with old versions ofbase
. No complex migration strategy, no CPP needed.Performance gains (very minor). Single method classes are currently represented more efficiently in GHC. In some higher-order cases removing the method might speed up programs. Also, compiling
base
and libraries withEq
instances and creating haddocks gets (very slightly) faster, because there is less to do.Implementation in
base
is simple: See https://gitlab.haskell.org/ghc/ghc/-/merge_requests/6793.Why not (and rebuttals)
Most of these are taken from the
libraries
thread; rebuttals are mine.(/=)
can sometimes be implemented more efficiently.Rebuttal: Beyond the cost of a single call to
not
, which GHC often optimizes away anyways, this is not possible (for law-abiding instances.)(/=)
can sometimes be more natural to implement.Rebuttal: Relatively unlikely, and possibly more error-prone and harder to read, due to the many negations.
There is a one-time cost for affected library authors, and it’s not worth it.
A mildly naive hackage search shows 131 affected packages.
Rebuttal: The change is easy, mechanical and backward compatible. But more important: Because this change simplifies both
base
and the code of affected libraries, it will pay off eventually, because of all the work we (as the community) save over time by no longer having to think about(/=)
. In fact, the evidence that 131 packages’s authors manually (and, in most cases, pointlessy) wrote an(/=)
means this proposal will save the next 131 packages’s authors, and beyond, unnecessary work.Some libraries use this to write unlawful instances, e.g. Non-lawful Eq Null instance? haskellari/postgresql-simple#78.
Rebuttal: Don’t write unlawful instances. If you need
(/=)
to behave differently than the negation of(==)
,Eq
is the wrong type class. I understand that this is a bit harsh, as the libraries authors might have reasons to break the lawfulness.RULE matching on single method class methods doesn't work reliably, but does work for other classes.
_ Rebuttal_: I consider that a bug in GHC and would fix one way or another before enacting this proposal.
We should clean up the type class hierarchy in a bigger way.
Rebuttal: Maybe, but that should not doing this small cleanup proposed here.
The text was updated successfully, but these errors were encountered: