From e0801bef9a38333935d4c50ae511663036b90d3e Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 21:30:33 +0200 Subject: [PATCH 01/10] Add a `DiffError` constructor to include a textual representation of a top-level difference --- generic-diff/src/Generics/Diff/Type.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/generic-diff/src/Generics/Diff/Type.hs b/generic-diff/src/Generics/Diff/Type.hs index 519493f..38981e5 100644 --- a/generic-diff/src/Generics/Diff/Type.hs +++ b/generic-diff/src/Generics/Diff/Type.hs @@ -4,6 +4,7 @@ module Generics.Diff.Type where import Data.List.NonEmpty (NonEmpty (..)) import Data.SOP.NP +import Data.Text (Text) import qualified Data.Text.Lazy.Builder as TB import Generics.SOP as SOP import Numeric.Natural @@ -25,6 +26,8 @@ See 'SpecialDiff'. data DiffError a where -- | All we can say is that the values being compared are not equal. TopLevelNotEqual :: DiffError a + -- | Same as 'TopLevelNotEqual', but with a textual representation of the values that differ. + TopLevelNotEqualShow :: Text -> Text -> DiffError a -- | We've identified a diff at a certain constructor or field Nested :: DiffErrorNested (Code a) -> DiffError a -- | Special case for special cases From 81c8fba72f90b68ef7991a17559b6a70cb9f9ff7 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 21:31:19 +0200 Subject: [PATCH 02/10] Include the values when we render a `TopLevelNotEqualShow` --- generic-diff/src/Generics/Diff/Render.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/generic-diff/src/Generics/Diff/Render.hs b/generic-diff/src/Generics/Diff/Render.hs index d4dee01..8cfc483 100644 --- a/generic-diff/src/Generics/Diff/Render.hs +++ b/generic-diff/src/Generics/Diff/Render.hs @@ -106,6 +106,12 @@ diffResultDoc = \case diffErrorDoc :: forall a. DiffError a -> Doc diffErrorDoc = \case TopLevelNotEqual -> linesDoc (pure "Not equal") + TopLevelNotEqualShow l r -> + linesDoc $ + "Not equal" + :| [ "Left value: " <> TB.fromText l + , "Right value: " <> TB.fromText r + ] Nested err -> diffErrorNestedDoc err DiffSpecial err -> renderSpecialDiffError @a err From c444ad826382271aa05de2edff6773aa8df698d6 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 21:48:31 +0200 Subject: [PATCH 03/10] Variants of `eqDiff` and `gdiffTopLevel` that use the new constructor --- generic-diff/src/Generics/Diff.hs | 2 ++ generic-diff/src/Generics/Diff/Class.hs | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/generic-diff/src/Generics/Diff.hs b/generic-diff/src/Generics/Diff.hs index d7f68cf..5397df2 100644 --- a/generic-diff/src/Generics/Diff.hs +++ b/generic-diff/src/Generics/Diff.hs @@ -172,8 +172,10 @@ module Generics.Diff -- ** Implementing diff , gdiff + , gdiffTopLevelShow , gdiffTopLevel , gdiffWith + , eqDiffShow , eqDiff -- * Types diff --git a/generic-diff/src/Generics/Diff/Class.hs b/generic-diff/src/Generics/Diff/Class.hs index 8a37911..44375f8 100644 --- a/generic-diff/src/Generics/Diff/Class.hs +++ b/generic-diff/src/Generics/Diff/Class.hs @@ -7,8 +7,10 @@ module Generics.Diff.Class -- ** Implementing diff , gdiff + , gdiffTopLevelShow , gdiffTopLevel , gdiffWith + , eqDiffShow , eqDiff , diffWithSpecial , gspecialDiffNested @@ -20,6 +22,7 @@ where import Data.SOP import Data.SOP.NP +import qualified Data.Text as T import qualified GHC.Generics as G import Generics.Diff.Render import Generics.Diff.Type @@ -169,6 +172,19 @@ eqDiff a b = then Equal else Error TopLevelNotEqual +tshow :: (Show a) => a -> T.Text +tshow = T.pack . show +{-# INLINE tshow #-} + +{- | Like 'eqDiff', 'eqDiffShow' only compares the values at the top-level. It also includes +a textual representation of the compared values. +-} +eqDiffShow :: (Eq a, Show a) => a -> a -> DiffResult a +eqDiffShow a b = + if a == b + then Equal + else Error $ TopLevelNotEqualShow (tshow a) (tshow b) + {- | The default implementation of 'diff'. Follows the procedure described above. We keep recursing into the 'Diff' instances of the field types, as far as we can. -} @@ -191,6 +207,15 @@ gdiffTopLevel :: DiffResult a gdiffTopLevel = gdiffWithPure @a @Eq (Differ eqDiff) +-- | Same as 'gdiffTopLevel', but includes a textual representation of the compared values. +gdiffTopLevelShow :: + forall a. + (Generic a, HasDatatypeInfo a, All2 (And Eq Show) (Code a)) => + a -> + a -> + DiffResult a +gdiffTopLevelShow = gdiffWithPure @a @(And Eq Show) (Differ eqDiffShow) + {- | Follow the same algorithm as 'gdiff', but the caller can provide their own 'POP' grid of 'Differ's specifying how to compare each field we might come across. -} From 6f6b6d38cf4e8d0bab6180597f1dbc4085d1860e Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 21:49:15 +0200 Subject: [PATCH 04/10] Use `eqDiffShow` in all `Diff` instances for types that have `Show` instances --- generic-diff/src/Generics/Diff/Instances.hs | 202 ++++++++++---------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/generic-diff/src/Generics/Diff/Instances.hs b/generic-diff/src/Generics/Diff/Instances.hs index 06fe0ce..d5e32a4 100644 --- a/generic-diff/src/Generics/Diff/Instances.hs +++ b/generic-diff/src/Generics/Diff/Instances.hs @@ -96,75 +96,75 @@ import qualified Type.Reflection as TR instance Diff Void where diff = \case {} instance Diff () where diff () () = Equal instance Diff (Proxy a) where diff Proxy Proxy = Equal -instance Diff Bool where diff = eqDiff -instance Diff Ordering where diff = eqDiff -instance Diff Double where diff = eqDiff -instance Diff Float where diff = eqDiff -instance Diff Int where diff = eqDiff -instance Diff Int8 where diff = eqDiff -instance Diff Int16 where diff = eqDiff -instance Diff Int32 where diff = eqDiff -instance Diff Int64 where diff = eqDiff -instance Diff Word where diff = eqDiff -instance Diff Word8 where diff = eqDiff -instance Diff Word16 where diff = eqDiff -instance Diff Word32 where diff = eqDiff -instance Diff Word64 where diff = eqDiff -instance Diff Version where diff = eqDiff +instance Diff Bool where diff = eqDiffShow +instance Diff Ordering where diff = eqDiffShow +instance Diff Double where diff = eqDiffShow +instance Diff Float where diff = eqDiffShow +instance Diff Int where diff = eqDiffShow +instance Diff Int8 where diff = eqDiffShow +instance Diff Int16 where diff = eqDiffShow +instance Diff Int32 where diff = eqDiffShow +instance Diff Int64 where diff = eqDiffShow +instance Diff Word where diff = eqDiffShow +instance Diff Word8 where diff = eqDiffShow +instance Diff Word16 where diff = eqDiffShow +instance Diff Word32 where diff = eqDiffShow +instance Diff Word64 where diff = eqDiffShow +instance Diff Version where diff = eqDiffShow instance Diff Char where - diff = eqDiff - diffList = eqDiff + diff = eqDiffShow + diffList = eqDiffShow -instance (Eq a) => Diff (Ratio a) where diff = eqDiff -instance Diff Integer where diff = eqDiff -instance Diff ThreadId where diff = eqDiff +instance (Eq a, Show a) => Diff (Ratio a) where diff = eqDiffShow +instance Diff Integer where diff = eqDiffShow +instance Diff ThreadId where diff = eqDiffShow instance Diff (Chan a) where diff = eqDiff instance Diff (MVar a) where diff = eqDiff instance Diff (IORef a) where diff = eqDiff -instance Diff TypeRep where diff = eqDiff -instance Diff (TR.TypeRep a) where diff = eqDiff -instance Diff TyCon where diff = eqDiff +instance Diff TypeRep where diff = eqDiffShow +instance Diff (TR.TypeRep a) where diff = eqDiffShow +instance Diff TyCon where diff = eqDiffShow instance Diff (STRef s a) where diff = eqDiff instance Diff Unique where diff = eqDiff -instance Diff (ForeignPtr a) where diff = eqDiff -instance Diff (Ptr a) where diff = eqDiff -instance Diff (FunPtr a) where diff = eqDiff -instance Diff IntPtr where diff = eqDiff -instance Diff WordPtr where diff = eqDiff +instance Diff (ForeignPtr a) where diff = eqDiffShow +instance Diff (Ptr a) where diff = eqDiffShow +instance Diff (FunPtr a) where diff = eqDiffShow +instance Diff IntPtr where diff = eqDiffShow +instance Diff WordPtr where diff = eqDiffShow instance Diff (StablePtr a) where diff = eqDiff -instance Diff Handle where diff = eqDiff -instance Diff HandlePosn where diff = eqDiff +instance Diff Handle where diff = eqDiffShow +instance Diff HandlePosn where diff = eqDiffShow instance Diff (StableName a) where diff = eqDiff instance Diff (TVar a) where diff = eqDiff -instance Diff Natural where diff = eqDiff -instance Diff Event where diff = eqDiff - -instance Diff CChar where diff = eqDiff -instance Diff CSChar where diff = eqDiff -instance Diff CUChar where diff = eqDiff -instance Diff CShort where diff = eqDiff -instance Diff CUShort where diff = eqDiff -instance Diff CInt where diff = eqDiff -instance Diff CUInt where diff = eqDiff -instance Diff CLong where diff = eqDiff -instance Diff CULong where diff = eqDiff -instance Diff CPtrdiff where diff = eqDiff -instance Diff CSize where diff = eqDiff -instance Diff CWchar where diff = eqDiff -instance Diff CSigAtomic where diff = eqDiff -instance Diff CLLong where diff = eqDiff -instance Diff CULLong where diff = eqDiff -instance Diff CIntPtr where diff = eqDiff -instance Diff CUIntPtr where diff = eqDiff -instance Diff CIntMax where diff = eqDiff -instance Diff CUIntMax where diff = eqDiff -instance Diff CClock where diff = eqDiff -instance Diff CTime where diff = eqDiff -instance Diff CUSeconds where diff = eqDiff -instance Diff CSUSeconds where diff = eqDiff -instance Diff CFloat where diff = eqDiff -instance Diff CDouble where diff = eqDiff +instance Diff Natural where diff = eqDiffShow +instance Diff Event where diff = eqDiffShow + +instance Diff CChar where diff = eqDiffShow +instance Diff CSChar where diff = eqDiffShow +instance Diff CUChar where diff = eqDiffShow +instance Diff CShort where diff = eqDiffShow +instance Diff CUShort where diff = eqDiffShow +instance Diff CInt where diff = eqDiffShow +instance Diff CUInt where diff = eqDiffShow +instance Diff CLong where diff = eqDiffShow +instance Diff CULong where diff = eqDiffShow +instance Diff CPtrdiff where diff = eqDiffShow +instance Diff CSize where diff = eqDiffShow +instance Diff CWchar where diff = eqDiffShow +instance Diff CSigAtomic where diff = eqDiffShow +instance Diff CLLong where diff = eqDiffShow +instance Diff CULLong where diff = eqDiffShow +instance Diff CIntPtr where diff = eqDiffShow +instance Diff CUIntPtr where diff = eqDiffShow +instance Diff CIntMax where diff = eqDiffShow +instance Diff CUIntMax where diff = eqDiffShow +instance Diff CClock where diff = eqDiffShow +instance Diff CTime where diff = eqDiffShow +instance Diff CUSeconds where diff = eqDiffShow +instance Diff CSUSeconds where diff = eqDiffShow +instance Diff CFloat where diff = eqDiffShow +instance Diff CDouble where diff = eqDiffShow instance Diff E0 instance Diff E1 @@ -174,72 +174,72 @@ instance Diff E6 instance Diff E9 instance Diff E12 -instance Diff T.Text where diff = eqDiff -instance Diff TL.Text where diff = eqDiff -instance Diff TLB.Builder where diff = eqDiff -instance Diff UnicodeException where diff = eqDiff - -instance Diff ArithException where diff = eqDiff -instance Diff ArrayException where diff = eqDiff -instance Diff Associativity where diff = eqDiff -instance Diff AsyncException where diff = eqDiff -instance Diff BlockReason where diff = eqDiff -instance Diff BufferMode where diff = eqDiff +instance Diff T.Text where diff = eqDiffShow +instance Diff TL.Text where diff = eqDiffShow +instance Diff TLB.Builder where diff = eqDiffShow +instance Diff UnicodeException where diff = eqDiffShow + +instance Diff ArithException where diff = eqDiffShow +instance Diff ArrayException where diff = eqDiffShow +instance Diff Associativity where diff = eqDiffShow +instance Diff AsyncException where diff = eqDiffShow +instance Diff BlockReason where diff = eqDiffShow +instance Diff BufferMode where diff = eqDiffShow instance Diff BufferState where diff = eqDiff #if MIN_VERSION_base(4,17,0) -instance Diff ByteArray where diff = eqDiff +instance Diff ByteArray where diff = eqDiffShow #endif -instance Diff ByteOrder where diff = eqDiff -instance Diff CBool where diff = eqDiff -instance Diff (Coercion a b) where diff = eqDiff +instance Diff ByteOrder where diff = eqDiffShow +instance Diff CBool where diff = eqDiffShow +instance Diff (Coercion a b) where diff = eqDiffShow #if MIN_VERSION_base(4,18,0) -instance Diff (ConstPtr a) where diff = eqDiff +instance Diff (ConstPtr a) where diff = eqDiffShow #endif -instance Diff DataRep where diff = eqDiff -instance Diff G.DecidedStrictness where diff = eqDiff +instance Diff DataRep where diff = eqDiffShow +instance Diff G.DecidedStrictness where diff = eqDiffShow instance Diff Errno where diff = eqDiff -instance Diff ErrorCall where diff = eqDiff -instance Diff ExitCode where diff = eqDiff -instance Diff FdKey where diff = eqDiff -instance Diff Fingerprint where diff = eqDiff -instance Diff G.Fixity where diff = eqDiff -instance Diff GeneralCategory where diff = eqDiff +instance Diff ErrorCall where diff = eqDiffShow +instance Diff ExitCode where diff = eqDiffShow +instance Diff FdKey where diff = eqDiffShow +instance Diff Fingerprint where diff = eqDiffShow +instance Diff G.Fixity where diff = eqDiffShow +instance Diff GeneralCategory where diff = eqDiffShow #if MIN_VERSION_base(4,18,0) -instance Diff InfoProv where diff = eqDiff +instance Diff InfoProv where diff = eqDiffShow #endif instance Diff (IOArray i e) where diff = eqDiff instance Diff IODeviceType where diff = eqDiff -instance Diff IOErrorType where diff = eqDiff -instance Diff IOException where diff = eqDiff -instance Diff IOMode where diff = eqDiff -instance Diff Lexeme where diff = eqDiff -instance Diff Lifetime where diff = eqDiff -instance Diff MaskingState where diff = eqDiff +instance Diff IOErrorType where diff = eqDiffShow +instance Diff IOException where diff = eqDiffShow +instance Diff IOMode where diff = eqDiffShow +instance Diff Lexeme where diff = eqDiffShow +instance Diff Lifetime where diff = eqDiffShow +instance Diff MaskingState where diff = eqDiffShow #if MIN_VERSION_base(4,17,0) instance Diff (MutableByteArray a) where diff = eqDiff #endif -instance Diff NewlineMode where diff = eqDiff -instance Diff Newline where diff = eqDiff +instance Diff NewlineMode where diff = eqDiffShow +instance Diff Newline where diff = eqDiffShow #if MIN_VERSION_base(4,16,0) -instance Diff (OrderingI a b) where diff = eqDiff +instance Diff (OrderingI a b) where diff = eqDiffShow #endif -instance Diff SeekMode where diff = eqDiff +instance Diff SeekMode where diff = eqDiffShow #if MIN_VERSION_base(4,16,0) -instance Diff SomeChar where diff = eqDiff +instance Diff SomeChar where diff = eqDiffShow #endif -instance Diff SomeNat where diff = eqDiff -instance Diff SomeSymbol where diff = eqDiff -instance Diff SrcLoc where diff = eqDiff +instance Diff SomeNat where diff = eqDiffShow +instance Diff SomeSymbol where diff = eqDiffShow +instance Diff SrcLoc where diff = eqDiffShow #if MIN_VERSION_base(4,17,0) -instance Diff StackEntry where diff = eqDiff +instance Diff StackEntry where diff = eqDiffShow #endif instance Diff (STArray s i a) where diff = eqDiff -instance Diff ThreadStatus where diff = eqDiff +instance Diff ThreadStatus where diff = eqDiffShow instance Diff TimeoutKey where diff = eqDiff #if MIN_VERSION_base(4,14,0) -instance Diff Timeout where diff = eqDiff +instance Diff Timeout where diff = eqDiffShow #endif -instance Diff TrName where diff = eqDiff +instance Diff TrName where diff = eqDiffShow {- FOURMOLU_ENABLE -} From 8e8c55670534d04f6778b1a66f6ffcb3286d3286 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 22:06:32 +0200 Subject: [PATCH 05/10] Update documentation --- generic-diff/src/Generics/Diff.hs | 16 +++++++++++----- generic-diff/src/Generics/Diff/Class.hs | 17 ++++++++++++++--- generic-diff/src/Generics/Diff/Render.hs | 6 ++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/generic-diff/src/Generics/Diff.hs b/generic-diff/src/Generics/Diff.hs index 5397df2..7c2f323 100644 --- a/generic-diff/src/Generics/Diff.hs +++ b/generic-diff/src/Generics/Diff.hs @@ -127,13 +127,16 @@ uses the @Right@ constructor"! And of course, once we have one step of recursion The 'Diff' class encapsulates the above behaviour with 'diff'. It's very strongly recommended that you don't implement 'diff' yourself, but use the default implementation using 'Generics.SOP.Generic', which is just 'gdiff'. -In case you might want to implement 'diff' yourself, there are three other functions you might want to use. +In case you might want to implement 'diff' yourself, there are five other functions you might want to use. -- 'eqDiff' simply delegates the entire process to '(==)', and will only ever give 'Equal' or 'TopLevelNotEqual'. This is -no more useful than 'Eq', and should only be used for primitive types (e.g. all numeric types like 'Char' and 'Int') -use 'eqDiff', since they don't really have ADTs or recursion. +- 'eqDiffShow' simply delegates the entire process to '(==)', and will only ever give 'Equal' or 'TopLevelNotEqualShow'. This is +no more useful than 'Eq', and should only be used for primitive types (e.g. all numeric types like 'Char' and 'Int' +use 'eqDiff', since they don't really have ADTs or recursion). -- 'gdiffTopLevel' does the above process, but without recursion. In other words each pair of fields is compared using +- 'eqDiff' is the same as 'eqDiffShow', but uses 'TopLevelNotEqual'. This should only be used for primitive types that +do not have 'Show' instances. + +- 'gdiffTopLevelShow' does the above process, but without recursion. In other words each pair of fields is compared using '(==)'. This is definitely better than 'Eq', by one "level". One situation when this might be useful is when your type refers to types from other libraries, and you want to avoid orphan 'Diff' instances for those types. Another is when the types of the fields are "small" enough that we don't care about recursing into them. For example: @@ -159,6 +162,9 @@ instance 'Diff' Request where 'diff' = 'gdiffTopLevel' @ +- 'gdiffTopLevel' is the same as 'gdiffTopLevelShow', but uses 'eqDiffShow' instead of 'eqDiff'. This should only be used +for primitive types that do not have 'Show' instances. + - 'diffWithSpecial' lets us handle edge cases for funky types with unusual 'Eq' instances or preserved invariants. See "Generics.Diff.Special". diff --git a/generic-diff/src/Generics/Diff/Class.hs b/generic-diff/src/Generics/Diff/Class.hs index 44375f8..6b212b8 100644 --- a/generic-diff/src/Generics/Diff/Class.hs +++ b/generic-diff/src/Generics/Diff/Class.hs @@ -71,7 +71,7 @@ ghci> diff Plus Minus Error (Nested (WrongConstructor (Z (Constructor \"Plus\")) (S (Z (Constructor \"Minus\"))))) ghci> diff (Atom 1) (Atom 2) -Error (Nested (FieldMismatch (AtLoc (Z (Constructor \"Atom\" :*: Z (Nested TopLevelNotEqual)))))) +Error (Nested (FieldMismatch (AtLoc (Z (Constructor \"Atom\" :*: Z (Nested $ TopLevelNotEqualShow \"1\" \"2\")))))) ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Atom 2) Error (Nested (WrongConstructor (S (Z (Constructor \"Bin\"))) (Z (Constructor \"Atom\")))) @@ -80,7 +80,7 @@ ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Minus (Atom 1)) Error (Nested (FieldMismatch (AtLoc (S (Z (Constructor \"Bin\" :*: S (Z (Nested (WrongConstructor (Z (Constructor \"Plus\")) (S (Z (Constructor \"Minus\")))))))))))) ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Plus (Atom 2)) -Error (Nested (FieldMismatch (DiffAtField (S (Z (Record \"Bin\" (FieldInfo \"left\" :* FieldInfo \"op\" :* FieldInfo \"right\" :* Nil) :*: S (S (Z (Nested (FieldMismatch (DiffAtField (Z (Constructor \"Atom\" :*: Z TopLevelNotEqual))))))))))))) +Error (Nested (FieldMismatch (DiffAtField (S (Z (Record \"Bin\" (FieldInfo \"left\" :* FieldInfo \"op\" :* FieldInfo \"right\" :* Nil) :*: S (S (Z (Nested (FieldMismatch (DiffAtField (Z (Constructor \"Atom\" :*: Z $ TopLevelNotEqualShow "1" "2"))))))))))))) @ Of course, these are just as difficult to understand as derived 'Show' instances, or more so. Fortunately we can @@ -99,6 +99,8 @@ ghci> printDiffResult $ diff (Atom 1) (Atom 2) Both values use constructor Atom but fields don't match In field 0 (0-indexed): Not equal + Left value: 1 + Right value: 2 ghci> printDiffResult $ diff (Bin (Atom 1) Plus (Atom 1)) (Atom 2) Wrong constructor @@ -118,6 +120,8 @@ In field right: Both values use constructor Atom but fields don't match In field 0 (0-indexed): Not equal + Left value: 1 + Right value: 2 @ = Laws @@ -130,7 +134,8 @@ x == y \<=\> x \`diff\` y == 'Equal' -} class Diff a where -- | Detailed comparison of two values. It is strongly recommended to only use the - -- default implementation, or one of 'eqDiff' or 'gdiffTopLevel'. + -- default implementation, or one of 'eqDiffShow' or 'gdiffTopLevelShow' + -- (or 'eqDiff' or 'gdiffTopLevel' if the type doesn't have a 'Show' instance). diff :: a -> a -> DiffResult a default diff :: (Generic a, HasDatatypeInfo a, All2 Diff (Code a)) => a -> a -> DiffResult a diff = gdiff @@ -165,6 +170,9 @@ diffListWith d = go 0 {- | The most basic 'Differ' possible. If the two values are equal, return 'Equal'; otherwise, return 'TopLevelNotEqual'. + +Note that if @a@ has a 'Show' instance, then 'eqDiffShow' is more informative since +it actually displays the compares values. -} eqDiff :: (Eq a) => a -> a -> DiffResult a eqDiff a b = @@ -198,6 +206,9 @@ gdiff = gdiffWithPure @a @Diff (Differ diff) {- | Alternate implementation of 'diff' - basically one level of 'gdiff'. To compare individual fields of the top-level values, we just use '(==)'. + +Note that if @a@ has a 'Show' instance, then 'eqDiffShow' is more informative since +it actually displays the compares values. -} gdiffTopLevel :: forall a. diff --git a/generic-diff/src/Generics/Diff/Render.hs b/generic-diff/src/Generics/Diff/Render.hs index 8cfc483..78513a9 100644 --- a/generic-diff/src/Generics/Diff/Render.hs +++ b/generic-diff/src/Generics/Diff/Render.hs @@ -125,6 +125,12 @@ ghci> 'TL.putStrLn' . 'TB.toLazyText' . 'renderDoc' 'defaultRenderOpts' 0 . 'lis Diff at list index 3 (0-indexed) Not equal +ghci> 'TL.putStrLn' . 'TB.toLazyText' . 'renderDoc' 'defaultRenderOpts' 0 . 'listDiffErrorDoc' "list" $ 'DiffAtIndex' 3 $ 'TopLevelNotEqual' \"1\" \"2\" +Diff at list index 3 (0-indexed) + Not equal + Left value: 1 + Right value: 2 + ghci> TL.putStrLn . TB.toLazyText . renderDoc defaultRenderOpts 0 . listDiffErrorDoc "non-empty list" $ WrongLengths 3 5 non-empty lists are wrong lengths Length of left list: 3 From 1e5d7895b0e64ae7fe9d9a3ee1e3fca2cc31d9ae Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 22:07:17 +0200 Subject: [PATCH 06/10] Update `generic-diff` tests --- generic-diff/test/Generics/Diff/UnitTestsSpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic-diff/test/Generics/Diff/UnitTestsSpec.hs b/generic-diff/test/Generics/Diff/UnitTestsSpec.hs index 71453a4..e2b645c 100644 --- a/generic-diff/test/Generics/Diff/UnitTestsSpec.hs +++ b/generic-diff/test/Generics/Diff/UnitTestsSpec.hs @@ -106,7 +106,7 @@ testSets = { setName = "Diff, FieldMismatch, Infix constructor, left side, nested" , leftValue = ('a', 4, ()) `Con3` [Just 1] , rightValue = ('a', 5, ()) `Con3` [Nothing, Just 1] - , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (S (S (Z (c3Info :*: Z (Nested $ FieldMismatch $ DiffAtField $ Z (Constructor "(,,)" :*: S (Z TopLevelNotEqual))))))))) + , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (S (S (Z (c3Info :*: Z (Nested $ FieldMismatch $ DiffAtField $ Z (Constructor "(,,)" :*: S (Z (TopLevelNotEqualShow "4" "5")))))))))) } , TestSet { setName = "Diff, FieldMismatch, recursive" From 3740b37fdcbf51480d9b7dab1d77ae2791ab84d0 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 22:11:41 +0200 Subject: [PATCH 07/10] Update `generic-diff-instances` tests --- .../test/Generics/Diff/UnitTestsSpec.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs b/generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs index 443c859..f2250fc 100644 --- a/generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs +++ b/generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs @@ -107,7 +107,7 @@ mapTestSets = value1 = Map.fromList [(1, "one"), (3, "three")] value2 = Map.fromList [(1, "one"), (3, "THREE")] - error2 = DiffSpecial $ Map.DiffAtKey 3 TopLevelNotEqual + error2 = DiffSpecial $ Map.DiffAtKey 3 $ TopLevelNotEqualShow "\"three\"" "\"THREE\"" value3 = Map.fromList [(1, "one"), (2, "two"), (3, "three")] error3 = DiffSpecial $ Map.LeftMissingKey 2 @@ -143,7 +143,7 @@ seqTestSets = error2 = DiffSpecial $ WrongLengths 2 3 value3 = Seq.fromList [1, 2] - error3 = DiffSpecial $ DiffAtIndex 1 TopLevelNotEqual + error3 = DiffSpecial $ DiffAtIndex 1 $ TopLevelNotEqualShow "3" "2" treeTestSets :: [TestSet (Tree Int)] treeTestSets = @@ -176,7 +176,7 @@ treeTestSets = value1 = Tree.Node 1 [Tree.Node 2 [], Tree.Node 3 [Tree.Node 4 [], Tree.Node 5 []]] value2 = Tree.Node 2 [] - error2 = DiffSpecial $ FieldMismatch $ DiffAtField $ Z $ nodeInfo :*: Z TopLevelNotEqual + error2 = DiffSpecial $ FieldMismatch $ DiffAtField $ Z $ nodeInfo :*: Z (TopLevelNotEqualShow "1" "2") value3 = Tree.Node 1 [Tree.Node 2 []] error3 = @@ -185,7 +185,7 @@ treeTestSets = value4 = Tree.Node 1 [Tree.Node 2 [], Tree.Node 4 []] error4 = - let e = DiffSpecial $ DiffAtIndex 1 $ DiffSpecial $ FieldMismatch $ DiffAtField $ Z $ nodeInfo :*: Z TopLevelNotEqual + let e = DiffSpecial $ DiffAtIndex 1 $ DiffSpecial $ FieldMismatch $ DiffAtField $ Z $ nodeInfo :*: Z (TopLevelNotEqualShow "3" "4") in DiffSpecial $ FieldMismatch $ DiffAtField $ Z $ nodeInfo :*: S (Z e) nodeInfo :: ConstructorInfo '[Int, [Tree Int]] @@ -222,10 +222,10 @@ customTreeTestSets = value1 = CustomTree $ Tree.Node 1 [Tree.Node 2 [], Tree.Node 3 [Tree.Node 4 [], Tree.Node 5 []]] value2 = CustomTree $ Tree.Node 2 [] - error2 = DiffSpecial $ DiffAtNode (TreePath []) TopLevelNotEqual + error2 = DiffSpecial $ DiffAtNode (TreePath []) $ TopLevelNotEqualShow "1" "2" value3 = CustomTree $ Tree.Node 1 [Tree.Node 2 []] error3 = DiffSpecial $ WrongLengthsOfChildren (TreePath []) 2 1 value4 = CustomTree $ Tree.Node 1 [Tree.Node 2 [], Tree.Node 4 []] - error4 = DiffSpecial $ DiffAtNode (TreePath [1]) TopLevelNotEqual + error4 = DiffSpecial $ DiffAtNode (TreePath [1]) $ TopLevelNotEqualShow "3" "4" From cbe2a754bee80fb17010cf2c30ec6b4c03c5e5af Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 22:18:07 +0200 Subject: [PATCH 08/10] Update changelog --- generic-diff/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/generic-diff/CHANGELOG.md b/generic-diff/CHANGELOG.md index de305ef..5d7fe10 100644 --- a/generic-diff/CHANGELOG.md +++ b/generic-diff/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Haskell Package Versioning Policy](https://pvp.hask ## [Unreleased] +### Changed + +- New `DiffError` constructor that includes a `Text` showing the two values compared at the top level in [#18](https://github.com/fpringle/generic-diff/pull/18). + ## [0.1.0.1] - 01.02.2026 ### Changed From ebd380648e610ae4a8f443f334684e4b9faa7e99 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Tue, 28 Apr 2026 22:36:50 +0200 Subject: [PATCH 09/10] Update README --- generic-diff/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/generic-diff/README.md b/generic-diff/README.md index 61d5ad4..a091505 100644 --- a/generic-diff/README.md +++ b/generic-diff/README.md @@ -42,4 +42,6 @@ In field right: Both values use constructor Atom but fields don't match In field 0 (0-indexed): Not equal + Left value: 1 + Right value: 2 ``` From 53a13e2ea8d4e514e204dc63029e7fb95bf30634 Mon Sep 17 00:00:00 2001 From: Frederick Pringle Date: Wed, 29 Apr 2026 00:14:37 +0200 Subject: [PATCH 10/10] Apply typo suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Berk Özkütük --- generic-diff/src/Generics/Diff/Class.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generic-diff/src/Generics/Diff/Class.hs b/generic-diff/src/Generics/Diff/Class.hs index 6b212b8..1369e7b 100644 --- a/generic-diff/src/Generics/Diff/Class.hs +++ b/generic-diff/src/Generics/Diff/Class.hs @@ -172,7 +172,7 @@ diffListWith d = go 0 otherwise, return 'TopLevelNotEqual'. Note that if @a@ has a 'Show' instance, then 'eqDiffShow' is more informative since -it actually displays the compares values. +it actually displays the compared values. -} eqDiff :: (Eq a) => a -> a -> DiffResult a eqDiff a b = @@ -207,8 +207,8 @@ gdiff = gdiffWithPure @a @Diff (Differ diff) {- | Alternate implementation of 'diff' - basically one level of 'gdiff'. To compare individual fields of the top-level values, we just use '(==)'. -Note that if @a@ has a 'Show' instance, then 'eqDiffShow' is more informative since -it actually displays the compares values. +Note that if @a@ has a 'Show' instance, then 'gdiffTopLevelShow' is more informative since +it actually displays the compared values. -} gdiffTopLevel :: forall a.