Skip to content

Commit 35ddc19

Browse files
authored
Merge pull request #14 from fpringle/fpringle/instances-library
Proper package for `Diff` instances
2 parents 2a69d23 + 5757cba commit 35ddc19

File tree

29 files changed

+139
-64
lines changed

29 files changed

+139
-64
lines changed

README.md

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,5 @@
11
[![Haskell CI](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml/badge.svg)](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml)
22

3-
# Generic structural diffs
3+
`generic-diff` provides a typeclass-based way to structurally compare values of the same type.
44

5-
`generic-diff` lets us pinpoint exactly where two values differ, which can be very useful, for example for debugging failing tests.
6-
This functionality is provided by the `Diff` typeclass, for which instances can be derived automatically using `Generic` from
7-
[generics-sop](https://github.com/well-typed/generics-sop).
8-
9-
For detailed information, see the [Hackage docs](https://hackage.haskell.org/package/generic-diff/docs/Generics-Diff.html).
10-
11-
## Example
12-
13-
```haskell
14-
{-# LANGUAGE DerivingStrategies #-}
15-
{-# LANGUAGE DeriveGeneric #-}
16-
{-# LANGUAGE DeriveAnyClass #-}
17-
18-
import Generics.Diff
19-
import Generics.Diff.Render
20-
21-
import qualified GHC.Generics as G
22-
import qualified Generics.SOP as SOP
23-
24-
data BinOp = Plus | Minus
25-
deriving stock (Show, G.Generic)
26-
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
27-
28-
data Expr
29-
= Atom Int
30-
| Bin {left :: Expr, op :: BinOp, right :: Expr}
31-
deriving stock (Show, G.Generic)
32-
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
33-
34-
expr1, expr2 :: Expr
35-
expr1 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 1))
36-
expr2 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 2))
37-
```
38-
39-
```haskell
40-
ghci> printDiffResult $ diff expr1 expr2
41-
In field right:
42-
Both values use constructor Bin but fields don't match
43-
In field right:
44-
Both values use constructor Atom but fields don't match
45-
In field 0 (0-indexed):
46-
Not equal
47-
```
5+
See [generic-diff](./generic-diff#readme) for the core package, or [generic-diff-instances](./generic-diff-instances#readme) for a more "batteries-included" set of instances.

cabal.project

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
packages:
2-
./generic-diff.cabal
3-
examples/containers-instances/generic-diff-containers.cabal
2+
./generic-diff/generic-diff.cabal
3+
./generic-diff-instances/generic-diff-instances.cabal
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
All notable changes to `generic-diff-instances` will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `Diff` instances for `Map`, `Seq`, `Set` and `Tree` from `containers`.
13+
14+
[unreleased]: https://github.com/fpringle/generic-diff/compare/7dbc273...HEAD
File renamed without changes.

generic-diff-instances/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[![Haskell CI](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml/badge.svg)](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml)
2+
3+
# `generic-diff` instances
4+
5+
The [generic-diff](https://hackage.haskell.org/package/generic-diff) package
6+
aims to be lightweight and not force any instances which might have more than
7+
one interpretation.
8+
9+
This package provides a more comprehensive set of instances for types from a
10+
range of common packages.
11+
12+
Currently we provide instances for [Map](https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Map-Lazy.html#t:Map), [Seq](https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Sequence.html#t:Seq), [Set](https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Set.html#t:Set) and [Tree](https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Tree.html#t:Tree) from the containers package.

examples/containers-instances/generic-diff-containers.cabal renamed to generic-diff-instances/generic-diff-instances.cabal

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
cabal-version: 3.0
2-
name: generic-diff-containers
2+
name: generic-diff-instances
33
version: 0.1.0.0
4+
synopsis: Diff instances for common types
5+
description:
6+
The [generic-diff](https://hackage.haskell.org/package/generic-diff) package
7+
aims to be lightweight and not force any instances which might have more than
8+
one interpretation.
9+
10+
This package provides a more comprehensive set of instances for types from a
11+
range of common packages.
412
license: BSD-3-Clause
513
author: Frederick Pringle
614
maintainer: freddyjepringle@gmail.com
715
copyright: Copyright(c) Frederick Pringle 2025
816
homepage: https://github.com/fpringle/generic-diff
17+
category: Generics, Test
918
build-type: Simple
19+
extra-doc-files: CHANGELOG.md
20+
README.md
1021
tested-with:
1122
GHC == 9.12.2
1223
GHC == 9.10.1
@@ -24,11 +35,11 @@ common warnings
2435
common deps
2536
build-depends:
2637
, base >= 4.12 && < 5
27-
, generic-diff
38+
, generic-diff >= 0.1 && < 0.2
2839
, sop-core >= 0.4.0.1 && < 0.6
2940
, generics-sop >= 0.4 && < 0.6
3041
, text >= 1.1 && < 2.2
31-
, containers
42+
, containers >= 0.5.9.2 && < 0.9
3243

3344
common extensions
3445
default-extensions:
@@ -67,7 +78,7 @@ library
6778
hs-source-dirs: src
6879
default-language: Haskell2010
6980

70-
test-suite generic-diff-containers-test
81+
test-suite generic-diff-instances-test
7182
import:
7283
warnings
7384
, deps
@@ -85,6 +96,6 @@ test-suite generic-diff-containers-test
8596
ghc-options: -Wno-orphans
8697
build-depends:
8798
, generic-diff
88-
, generic-diff-containers
99+
, generic-diff-instances
89100
, QuickCheck
90101
, hspec

examples/containers-instances/src/Generics/Diff/Special/Map.hs renamed to generic-diff-instances/src/Generics/Diff/Special/Map.hs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ data MapDiffError k v
3030
deriving (Show, Eq)
3131

3232
{- | Render a 'MapDiffError'. This is a top-level function because we'll use it in the implementations
33-
of 'renderSpecialDiffError' for both 'Map' and 'IntMap'.
33+
of 'renderSpecialDiffError' for both 'Map' and 'Data.IntMap.IntMap'.
3434
-}
3535
mapDiffErrorDoc :: (Show k) => MapDiffError k v -> Doc
3636
mapDiffErrorDoc = \case
@@ -43,9 +43,6 @@ mapDiffErrorDoc = \case
4343
RightMissingKey k ->
4444
linesDoc $ pure $ "The left map contains key " <> showB k <> " but the right doesn't"
4545

46-
------------------------------------------------------------
47-
-- Map
48-
4946
instance (Show k, Ord k, Diff v) => SpecialDiff (Map k v) where
5047
type SpecialDiffError (Map k v) = MapDiffError k v
5148

@@ -67,6 +64,5 @@ instance (Show k, Ord k, Diff v) => SpecialDiff (Map k v) where
6764

6865
renderSpecialDiffError = mapDiffErrorDoc
6966

70-
-- | Now we can implement 'Diff' using 'diffWithSpecial'.
7167
instance (Show k, Ord k, Diff v) => Diff (Map k v) where
7268
diff = diffWithSpecial

examples/containers-instances/src/Generics/Diff/Special/Seq.hs renamed to generic-diff-instances/src/Generics/Diff/Special/Seq.hs

File renamed without changes.

examples/containers-instances/src/Generics/Diff/Special/Set.hs renamed to generic-diff-instances/src/Generics/Diff/Special/Set.hs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ data SetDiffError k
3131
deriving (Show, Eq)
3232

3333
{- | Render a 'SetDiffError'. This is a top-level function because we'll use it in the implementations
34-
of 'renderSpecialDiffError' for both 'Set' and 'IntSet'.
34+
of 'renderSpecialDiffError' for both 'Set' and 'Data.IntSet.IntSet'.
3535
3636
There are no nested 'DiffError's here, so we use 'linesDoc'.
3737
-}
@@ -42,9 +42,8 @@ setDiffErrorDoc = \case
4242
RightMissingKey k ->
4343
linesDoc $ pure $ "The left set contains key " <> showB k <> " but the right doesn't"
4444

45-
{- | First we define an instance of 'SpecialDiff'. We need 'Show' and 'Eq' so that 'SetDiffError'
46-
also has these instances; we need 'Ord' to compare elements of the set.
47-
-}
45+
-- First we define an instance of 'SpecialDiff'. We need 'Show' and 'Eq' so that 'SetDiffError'
46+
-- also has these instances; we need 'Ord' to compare elements of the set.
4847
instance (Show k, Eq k, Ord k) => SpecialDiff (Set k) where
4948
type SpecialDiffError (Set k) = SetDiffError k
5049

@@ -62,6 +61,6 @@ instance (Show k, Eq k, Ord k) => SpecialDiff (Set k) where
6261

6362
renderSpecialDiffError = setDiffErrorDoc
6463

65-
-- | Now we can implement 'Diff' using 'diffWithSpecial'.
64+
-- Now we can implement 'Diff' using 'diffWithSpecial'.
6665
instance (Show k, Ord k) => Diff (Set k) where
6766
diff = diffWithSpecial

examples/containers-instances/src/Generics/Diff/Special/Tree.hs renamed to generic-diff-instances/src/Generics/Diff/Special/Tree.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{-# LANGUAGE DerivingVia #-}
22
{-# OPTIONS_GHC -Wno-orphans #-}
33

4-
{- | A worked example of implementing 'SpecialDiff' (and thereby 'Diff') for 'Tree's.
4+
{- | A worked example of implementing 'SpecialDiff' (and thereby 'Diff') for 'Tree.Tree's.
55
66
As with other 3rd-party types, there are different approaches we can take here. We'll show 2 of them:
77
@@ -22,6 +22,7 @@ import Generics.SOP.GGP
2222
------------------------------------------------------------
2323
-- Using gspecialDiffNested
2424

25+
-- | Generically-derived instance.
2526
instance (Diff a) => SpecialDiff (Tree.Tree a) where
2627
type SpecialDiffError (Tree.Tree a) = DiffErrorNested (GCode (Tree.Tree a))
2728
specialDiff = gspecialDiffNested
@@ -33,17 +34,23 @@ instance (Diff a) => Diff (Tree.Tree a) where
3334
------------------------------------------------------------
3435
-- Using SpecialDiff
3536

37+
{- | A newtype wrapper around 'Tree.Tree' to demonstrate one alternate way we could hand-write
38+
a 'SpecialDiff' instance.
39+
-}
3640
newtype CustomTree a = CustomTree (Tree.Tree a)
3741
deriving (Show) via (Tree.Tree a)
3842

43+
-- | Where are we in the tree? Each element of the list says which child node we step to next.
3944
newtype TreePath = TreePath [Int]
4045
deriving (Show, Eq) via [Int]
4146

47+
-- | A custom error type for 'CustomTree'.
4248
data CustomTreeDiffError a
4349
= DiffAtNode TreePath (DiffError a)
4450
| WrongLengthsOfChildren TreePath Int Int
4551
deriving (Show, Eq)
4652

53+
-- | Render a tree path as a 'TB.Builder'
4754
renderTreePath :: TreePath -> TB.Builder
4855
renderTreePath (TreePath []) = "<root>"
4956
renderTreePath (TreePath (x : xs)) = mconcat $ showB x : ["->" <> showB y | y <- xs]

0 commit comments

Comments
 (0)