diff --git a/README.md b/README.md index a25177b..726bfdb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cabal.project b/cabal.project index 1698ce8..46245a5 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/generic-diff-instances/CHANGELOG.md b/generic-diff-instances/CHANGELOG.md new file mode 100644 index 0000000..dd4040f --- /dev/null +++ b/generic-diff-instances/CHANGELOG.md @@ -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 diff --git a/LICENSE b/generic-diff-instances/LICENSE similarity index 100% rename from LICENSE rename to generic-diff-instances/LICENSE diff --git a/generic-diff-instances/README.md b/generic-diff-instances/README.md new file mode 100644 index 0000000..00277e6 --- /dev/null +++ b/generic-diff-instances/README.md @@ -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. diff --git a/examples/containers-instances/generic-diff-containers.cabal b/generic-diff-instances/generic-diff-instances.cabal similarity index 73% rename from examples/containers-instances/generic-diff-containers.cabal rename to generic-diff-instances/generic-diff-instances.cabal index 0082578..bd4a161 100644 --- a/examples/containers-instances/generic-diff-containers.cabal +++ b/generic-diff-instances/generic-diff-instances.cabal @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/examples/containers-instances/src/Generics/Diff/Special/Map.hs b/generic-diff-instances/src/Generics/Diff/Special/Map.hs similarity index 93% rename from examples/containers-instances/src/Generics/Diff/Special/Map.hs rename to generic-diff-instances/src/Generics/Diff/Special/Map.hs index 09065fd..020c35b 100644 --- a/examples/containers-instances/src/Generics/Diff/Special/Map.hs +++ b/generic-diff-instances/src/Generics/Diff/Special/Map.hs @@ -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 @@ -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 @@ -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 diff --git a/examples/containers-instances/src/Generics/Diff/Special/Seq.hs b/generic-diff-instances/src/Generics/Diff/Special/Seq.hs similarity index 100% rename from examples/containers-instances/src/Generics/Diff/Special/Seq.hs rename to generic-diff-instances/src/Generics/Diff/Special/Seq.hs diff --git a/examples/containers-instances/src/Generics/Diff/Special/Set.hs b/generic-diff-instances/src/Generics/Diff/Special/Set.hs similarity index 88% rename from examples/containers-instances/src/Generics/Diff/Special/Set.hs rename to generic-diff-instances/src/Generics/Diff/Special/Set.hs index e04a3d4..c660d53 100644 --- a/examples/containers-instances/src/Generics/Diff/Special/Set.hs +++ b/generic-diff-instances/src/Generics/Diff/Special/Set.hs @@ -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'. -} @@ -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 @@ -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 diff --git a/examples/containers-instances/src/Generics/Diff/Special/Tree.hs b/generic-diff-instances/src/Generics/Diff/Special/Tree.hs similarity index 88% rename from examples/containers-instances/src/Generics/Diff/Special/Tree.hs rename to generic-diff-instances/src/Generics/Diff/Special/Tree.hs index 436a202..b628e89 100644 --- a/examples/containers-instances/src/Generics/Diff/Special/Tree.hs +++ b/generic-diff-instances/src/Generics/Diff/Special/Tree.hs @@ -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: @@ -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 @@ -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 []) = "" renderTreePath (TreePath (x : xs)) = mconcat $ showB x : ["->" <> showB y | y <- xs] diff --git a/examples/containers-instances/test/Generics/Diff/PropertyTestsSpec.hs b/generic-diff-instances/test/Generics/Diff/PropertyTestsSpec.hs similarity index 100% rename from examples/containers-instances/test/Generics/Diff/PropertyTestsSpec.hs rename to generic-diff-instances/test/Generics/Diff/PropertyTestsSpec.hs diff --git a/examples/containers-instances/test/Generics/Diff/UnitTestsSpec.hs b/generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs similarity index 100% rename from examples/containers-instances/test/Generics/Diff/UnitTestsSpec.hs rename to generic-diff-instances/test/Generics/Diff/UnitTestsSpec.hs diff --git a/examples/containers-instances/test/Spec.hs b/generic-diff-instances/test/Spec.hs similarity index 100% rename from examples/containers-instances/test/Spec.hs rename to generic-diff-instances/test/Spec.hs diff --git a/examples/containers-instances/test/Util.hs b/generic-diff-instances/test/Util.hs similarity index 100% rename from examples/containers-instances/test/Util.hs rename to generic-diff-instances/test/Util.hs diff --git a/CHANGELOG.md b/generic-diff/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to generic-diff/CHANGELOG.md diff --git a/generic-diff/LICENSE b/generic-diff/LICENSE new file mode 100644 index 0000000..fc423c4 --- /dev/null +++ b/generic-diff/LICENSE @@ -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. diff --git a/generic-diff/README.md b/generic-diff/README.md new file mode 100644 index 0000000..61d5ad4 --- /dev/null +++ b/generic-diff/README.md @@ -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 +``` diff --git a/generic-diff.cabal b/generic-diff/generic-diff.cabal similarity index 94% rename from generic-diff.cabal rename to generic-diff/generic-diff.cabal index 1b05339..746b9d0 100644 --- a/generic-diff.cabal +++ b/generic-diff/generic-diff.cabal @@ -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 diff --git a/src/Generics/Diff.hs b/generic-diff/src/Generics/Diff.hs similarity index 100% rename from src/Generics/Diff.hs rename to generic-diff/src/Generics/Diff.hs diff --git a/src/Generics/Diff/Class.hs b/generic-diff/src/Generics/Diff/Class.hs similarity index 100% rename from src/Generics/Diff/Class.hs rename to generic-diff/src/Generics/Diff/Class.hs diff --git a/src/Generics/Diff/Instances.hs b/generic-diff/src/Generics/Diff/Instances.hs similarity index 100% rename from src/Generics/Diff/Instances.hs rename to generic-diff/src/Generics/Diff/Instances.hs diff --git a/src/Generics/Diff/Render.hs b/generic-diff/src/Generics/Diff/Render.hs similarity index 100% rename from src/Generics/Diff/Render.hs rename to generic-diff/src/Generics/Diff/Render.hs diff --git a/src/Generics/Diff/Special.hs b/generic-diff/src/Generics/Diff/Special.hs similarity index 100% rename from src/Generics/Diff/Special.hs rename to generic-diff/src/Generics/Diff/Special.hs diff --git a/src/Generics/Diff/Special/List.hs b/generic-diff/src/Generics/Diff/Special/List.hs similarity index 100% rename from src/Generics/Diff/Special/List.hs rename to generic-diff/src/Generics/Diff/Special/List.hs diff --git a/src/Generics/Diff/Type.hs b/generic-diff/src/Generics/Diff/Type.hs similarity index 97% rename from src/Generics/Diff/Type.hs rename to generic-diff/src/Generics/Diff/Type.hs index 170d611..519493f 100644 --- a/src/Generics/Diff/Type.hs +++ b/generic-diff/src/Generics/Diff/Type.hs @@ -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 diff --git a/test/Generics/Diff/PropertyTestsSpec.hs b/generic-diff/test/Generics/Diff/PropertyTestsSpec.hs similarity index 100% rename from test/Generics/Diff/PropertyTestsSpec.hs rename to generic-diff/test/Generics/Diff/PropertyTestsSpec.hs diff --git a/test/Generics/Diff/UnitTestsSpec.hs b/generic-diff/test/Generics/Diff/UnitTestsSpec.hs similarity index 100% rename from test/Generics/Diff/UnitTestsSpec.hs rename to generic-diff/test/Generics/Diff/UnitTestsSpec.hs diff --git a/test/Spec.hs b/generic-diff/test/Spec.hs similarity index 100% rename from test/Spec.hs rename to generic-diff/test/Spec.hs diff --git a/test/Util.hs b/generic-diff/test/Util.hs similarity index 100% rename from test/Util.hs rename to generic-diff/test/Util.hs