Automatically Migrating Eq of No (/=)
We’ve all spent more time talking about Eq
of no (/=)
than it deserves. Today Bodigrim published Migration guide for Eq of no (/=)
which describes all the steps you’ll need to take in order to update your codebase for the century of the fruitbat.
But that made me think — why do humans need to do this by hand? Computers are good at this sort of thing. So I wrote a tiny little comby config that does the replacements we want. Comby is a fantastic “parser parser combinator” — which is to say, a little DSL for writing program transformations. You just write the pattern you want to match, and comby lifts it to work over whitespace, and ensures that your greedy matches are parenthesis-aware, and that sort of thing. It’s quite lovely. The config I wrote is listed at the end of this post.
Here’s a problematic module that will be very broken by Eq
of no (/=)
:
module Neq where
import Prelude (Eq (..), Bool(..), (||))
import Data.Eq (Eq (..))
data A = A Bool Bool
instance Eq A where
A x1 x2 /= A y1 y2 = x1 /= y1 || x2 /= x2
data B = B Bool
instance Eq B where
B x == B y = x == y
B x /= B y = x /= y
data C a = C a
instance
Eq a => Eq (C a)
where
C x == C y = x == y
C x /= C y = x /= y
data D = D Bool
instance Eq D where
D x /= D y =
/= y
x D x == D y =
== y
x
data E = E Bool
instance Eq E where
E x /= E y =
let foo = x /= y in foo
After running comby
, we get the following diff:
module Neq where
-import Prelude (Eq (..), Bool)
-import Data.Eq (Eq (..))
-
-data A = A Bool
+import Prelude (Eq, (==), (/=), Bool(..), (||))
+import Data.Eq (Eq, (==), (/=))
+data A = A Bool Bool
instance Eq A where- A x1 x2 /= A y1 y2 = x1 /= y1 || x2 /= x2
-
+ A x1 x2 == A y1 y2 = not $ x1 /= y1 || x2 /= x2
data B = B Bool
instance Eq B where
B x == B y = x == y- B x /= B y = x /= y
data C a = C a
instance Eq a => Eq (C a) where
C x == C y = x == y- C x /= C y = x /= y
-
data D = D Bool
instance Eq D where- D x /= D y = x /= y
D x == D y = x == y
data E = E Bool
instance Eq E where- E x /= E y =
- let foo = x /= y in foo
+ E x == E y = not $ let foo = x /= y in foo
Is it perfect? No, but it’s pretty good for the 10 minutes it took me to write. A little effort here goes a long way!
My config file to automatically migrate Eq
of no (/=)
:
[only-neq]
match='''
instance :[ctx]Eq :[name] where
:[x] /= :[y] = :[z\n]
'''
rewrite='''
instance :[ctx]Eq :[name] where
:[x] == :[y] = not $ :[z]
'''
[both-eq-and-neq]
match='''
instance :[ctx]Eq :[name] where
:[x1] == :[y1] = :[z1\n]
:[x2] /= :[y2] = :[z2\n]
'''
rewrite='''
instance :[ctx]Eq :[name] where
:[x1] == :[y1] = :[z1]
'''
[both-neq-and-eq]
match='''
instance :[ctx]Eq :[name] where
:[x2] /= :[y2] = :[z2\n]
:[x1] == :[y1] = :[z1\n]
'''
rewrite='''
instance :[ctx]Eq :[name] where
:[x1] == :[y1] = :[z1]
'''
[import-prelude]
match='''
import Prelude (:[pre]Eq (..):[post])
'''
rewrite='''
import Prelude (:[pre]Eq, (==), (/=):[post])
'''
[import-data-eq]
match='''
import Data.Eq (:[pre]Eq (..):[post])
'''
rewrite='''
import Data.Eq (:[pre]Eq, (==), (/=):[post])
'''
Save this file as eq.toml
, and run comby
in your project root via:
$ comby -config eq.toml -matcher .hs -i -f .hs
Comby will find and make all the changes you need, in place. Check the diff, and make whatever changes you might need. In particular, it might bork some of your whitespace — there’s an issue to get comby to play more nicely with layout-aware languages. A more specialized tool that had better awareness of Haskell’s idiosyncrasies would help here, if you have some spare engineering cycles. But when all’s said and done, comby does a damn fine job.