I’d like to announce a new package candidate: generic-diff: Generic structural diffs
As the name implies, the package enables comparing two values of the same type, and pinpointing exactly where they differ. The main use-case I imagine is in testing, when we want to debug exactly why a test failed, but the derived Show
instance of the types we’re working with don’t lend themselves to easy inspection. For example if the output has a lot of parentheses, nesting etc, it’s not always easy to look at two values and spot where the difference is. Much more detailed motivation, with examples, can be found in Generics.Diff.
There is some prior art in this space: gdiff gives us an “Edit Script” which tells us how to transform one value into another (and therefore tells us all the differences between two values). This is much more comprehensive, but does require more boilerplate (Family
has to be implemented manually). By comparison, generic-diff
will stop at the first difference between two values; however it requires no boilerplate (just instances of Generic
and HasDatatypeInfo
from generics-sop).
One other nice feature of generic-diff
is the ability to pretty-print the diff types (which use sop-core
types, and therefore themselves can be quite difficult to read):
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
import Generics.Diff
import Generics.Diff.Render
import qualified GHC.Generics as G
import qualified Generics.SOP as SOP
data BinOp = Plus | Minus
deriving stock (Show, G.Generic)
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
data Expr
= Atom Int
| Bin {left :: Expr, op :: BinOp, right :: Expr}
deriving stock (Show, G.Generic)
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
expr1, expr2 :: Expr
expr1 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 1))
expr2 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 2))
ghci> printDiffResult $ diff expr1 expr2
In field right:
Both values use constructor Bin but fields don't match
In field right:
Both values use constructor Atom but fields don't match
In field 0 (0-indexed):
Not equal
The repo can be found here.
Any comments or suggestions are welcome