This defines the (==) and (/=) equality operators, and gives some default implementations (that mean we need only define one of the two to get both functions).

Now, equality only makes sense on a bounded set of types — not all types support it. Now, we can add a type to the set of types that support equality by writing an instance of Eq for it. For example, with Floats, we can define equality in terms of primitive machine equality on floats:

instanceEqFloatwhere(F#x)==(F#y)=x`eqFloat#`y

Or for unbounded integers, in terms of the GMP integer routines:

instanceEqIntegerwhere(==)=gmp_eqInteger(/=)=gmp_neqInteger

So now we can write:

main=doprint(pi==exp1)print(42==43-1)

And everything works nicely.

We get to reuse the same symbol, (==), for equality on any types that support it.

But that doesn’t let us do any overloading. What we need is a way to spot (=-=) and have the compiler replace it with the appropriate function, based on the method type.

And we can do this. GHC supports rewrite rules, which are used for a wide range of compile time optimizations, including sophisticated data structure transformations.

Normally, rewrite rules are used to match on a name, and replace the left hand side with some new right hand side.

For example,

{-# RULES
"map fusion" forall f g.
map f . map g == map (f . g)
#-}

Is the rule for map fusion, replacing two traversals of a list with one. A less used feature is the ability to match on an expression’s type on either the left or right hand side of a rewrite rule (the left and right hand sides must have the same type).

And all is good, well, except for the Int, which wasn’t resolved, since we didn’t write a rule for it.

We’d like to recover the property that real type classes have — that it’s a type error to use a type class method at a type other than those that support it. Thanks to Gwern Branwen for suggesting we try this.

The problem is: we want to replace any remnant calls to (=-=) with an expression that will fail to compile. However, we’re strongly constrained by the rewrite rules system — rewrite rules must always be syntactically and type correct. However, and this is the deliciously evil part, they don’t need to be confluent or terminating.

So we can encode the missing instance type error as a non-terminating rewrite rule! And then there’s no risk of runtime failure — as the compiler won’t terminate :-)

in the form of the compiler failed to terminate. As all good Haskellers know, this is morally sound: one bottom is as good as another, so a type error is as good as a divergent compiler! And well-typed programs are still not going to go wrong.

We can also encode the type error as a (compile time) linker failure, by replacing (=-=) with an unknown foreign symbol that we import irregardless. Of course, this is very evil.

Rewrite rules are a powerful way for users to extend the optimization suite of the compiler, and they’re made so by purity and static type information, enabling a richer set of possible transformations than would be possible otherwise.