Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 2 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,5 @@
[![Haskell CI](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml/badge.svg)](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml)

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

`generic-diff` lets us pinpoint exactly where two values differ, which can be very useful, for example for debugging failing tests.
This functionality is provided by the `Diff` typeclass, for which instances can be derived automatically using `Generic` from
[generics-sop](https://github.com/well-typed/generics-sop).

For detailed information, see the [Hackage docs](https://hackage.haskell.org/package/generic-diff/docs/Generics-Diff.html).

## Example

```haskell
{-# 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))
```

```haskell
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
```
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.
4 changes: 2 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
packages:
./generic-diff.cabal
examples/containers-instances/generic-diff-containers.cabal
./generic-diff/generic-diff.cabal
./generic-diff-instances/generic-diff-instances.cabal
14 changes: 14 additions & 0 deletions generic-diff-instances/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to `generic-diff-instances` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org).

## [Unreleased]

### Added

- `Diff` instances for `Map`, `Seq`, `Set` and `Tree` from `containers`.

[unreleased]: https://github.com/fpringle/generic-diff/compare/7dbc273...HEAD
File renamed without changes.
12 changes: 12 additions & 0 deletions generic-diff-instances/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[![Haskell CI](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml/badge.svg)](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml)

# `generic-diff` instances

The [generic-diff](https://hackage.haskell.org/package/generic-diff) package
aims to be lightweight and not force any instances which might have more than
one interpretation.

This package provides a more comprehensive set of instances for types from a
range of common packages.

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.
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
cabal-version: 3.0
name: generic-diff-containers
name: generic-diff-instances
version: 0.1.0.0
synopsis: Diff instances for common types
description:
The [generic-diff](https://hackage.haskell.org/package/generic-diff) package
aims to be lightweight and not force any instances which might have more than
one interpretation.

This package provides a more comprehensive set of instances for types from a
range of common packages.
license: BSD-3-Clause
author: Frederick Pringle
maintainer: freddyjepringle@gmail.com
copyright: Copyright(c) Frederick Pringle 2025
homepage: https://github.com/fpringle/generic-diff
category: Generics, Test
build-type: Simple
extra-doc-files: CHANGELOG.md
README.md
tested-with:
GHC == 9.12.2
GHC == 9.10.1
Expand All @@ -24,11 +35,11 @@ common warnings
common deps
build-depends:
, base >= 4.12 && < 5
, generic-diff
, generic-diff >= 0.1 && < 0.2
, sop-core >= 0.4.0.1 && < 0.6
, generics-sop >= 0.4 && < 0.6
, text >= 1.1 && < 2.2
, containers
, containers >= 0.5.9.2 && < 0.9

common extensions
default-extensions:
Expand Down Expand Up @@ -67,7 +78,7 @@ library
hs-source-dirs: src
default-language: Haskell2010

test-suite generic-diff-containers-test
test-suite generic-diff-instances-test
import:
warnings
, deps
Expand All @@ -85,6 +96,6 @@ test-suite generic-diff-containers-test
ghc-options: -Wno-orphans
build-depends:
, generic-diff
, generic-diff-containers
, generic-diff-instances
, QuickCheck
, hspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data MapDiffError k v
deriving (Show, Eq)

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

------------------------------------------------------------
-- Map

instance (Show k, Ord k, Diff v) => SpecialDiff (Map k v) where
type SpecialDiffError (Map k v) = MapDiffError k v

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

renderSpecialDiffError = mapDiffErrorDoc

-- | Now we can implement 'Diff' using 'diffWithSpecial'.
instance (Show k, Ord k, Diff v) => Diff (Map k v) where
diff = diffWithSpecial
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ data SetDiffError k
deriving (Show, Eq)

{- | Render a 'SetDiffError'. This is a top-level function because we'll use it in the implementations
of 'renderSpecialDiffError' for both 'Set' and 'IntSet'.
of 'renderSpecialDiffError' for both 'Set' and 'Data.IntSet.IntSet'.

There are no nested 'DiffError's here, so we use 'linesDoc'.
-}
Expand All @@ -42,9 +42,8 @@ setDiffErrorDoc = \case
RightMissingKey k ->
linesDoc $ pure $ "The left set contains key " <> showB k <> " but the right doesn't"

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

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

renderSpecialDiffError = setDiffErrorDoc

-- | Now we can implement 'Diff' using 'diffWithSpecial'.
-- Now we can implement 'Diff' using 'diffWithSpecial'.
instance (Show k, Ord k) => Diff (Set k) where
diff = diffWithSpecial
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{-# LANGUAGE DerivingVia #-}
{-# OPTIONS_GHC -Wno-orphans #-}

{- | A worked example of implementing 'SpecialDiff' (and thereby 'Diff') for 'Tree's.
{- | A worked example of implementing 'SpecialDiff' (and thereby 'Diff') for 'Tree.Tree's.

As with other 3rd-party types, there are different approaches we can take here. We'll show 2 of them:

Expand All @@ -22,6 +22,7 @@ import Generics.SOP.GGP
------------------------------------------------------------
-- Using gspecialDiffNested

-- | Generically-derived instance.
instance (Diff a) => SpecialDiff (Tree.Tree a) where
type SpecialDiffError (Tree.Tree a) = DiffErrorNested (GCode (Tree.Tree a))
specialDiff = gspecialDiffNested
Expand All @@ -33,17 +34,23 @@ instance (Diff a) => Diff (Tree.Tree a) where
------------------------------------------------------------
-- Using SpecialDiff

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

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

-- | A custom error type for 'CustomTree'.
data CustomTreeDiffError a
= DiffAtNode TreePath (DiffError a)
| WrongLengthsOfChildren TreePath Int Int
deriving (Show, Eq)

-- | Render a tree path as a 'TB.Builder'
renderTreePath :: TreePath -> TB.Builder
renderTreePath (TreePath []) = "<root>"
renderTreePath (TreePath (x : xs)) = mconcat $ showB x : ["->" <> showB y | y <- xs]
Expand Down
File renamed without changes.
30 changes: 30 additions & 0 deletions generic-diff/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Copyright (c) 2025, Frederick Pringle

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* Neither the name of Frederick Pringle nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 changes: 45 additions & 0 deletions generic-diff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generic structural diffs

`generic-diff` lets us pinpoint exactly where two values differ, which can be very useful, for example for debugging failing tests.
This functionality is provided by the `Diff` typeclass, for which instances can be derived automatically using `Generic` from
[generics-sop](https://github.com/well-typed/generics-sop).

For detailed information, see the [Hackage docs](https://hackage.haskell.org/package/generic-diff/docs/Generics-Diff.html).

## Example

```haskell
{-# 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))
```

```haskell
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
```
3 changes: 3 additions & 0 deletions generic-diff.cabal → generic-diff/generic-diff.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ description:
using [generics-sop](https://hackage.haskell.org/package/generics-sop).

See the module documentation in "Generics.Diff".

For a wider range of instances for common types, see
[generic-diff-instances](https://hackage.haskell.org/package/generic-diff-instances).
license: BSD-3-Clause
license-file: LICENSE
author: Frederick Pringle
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ such as lists (see 'ListDiffError'), or even user-defined types that internally
or have unusual 'Eq' instances. In this case we can implement an instance of 'SpecialDiff' for the
type.

For concrete examples implementing 'SpecialDiff' on types from "containers", see the
[examples/containers-instances](https://github.com/fpringle/generic-diff/tree/main/examples/containers-instances)
For concrete implementations of 'SpecialDiff' on types from "containers", see
[generic-diff-instances](https://hackage.haskell.org/package/generic-diff-instances).
directory.
-}
class (Show (SpecialDiffError a), Eq (SpecialDiffError a)) => SpecialDiff a where
Expand Down
File renamed without changes.
File renamed without changes.
Loading