{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-| Module : Data.Semialign.Diff Copyright : (c) 2019, Commonwealth Scientific and Industrial Research Organisation License : BSD3 Maintainer : jack.kelly@data61.csiro.au Stability : experimental Portability : Portable The 'Semialign' typeclass lets us line up two structures of the same type. It's then possible to take a simple diff by comparing the points of overlap. === A note on type variables The return type of the diffing functions is very general, because we might want to (say) diff two @[a]@ into an @'Data.IntMap.IntMap' a@, @'Data.Map.Map' Int a@ or some other structure. This generality can hurt type inference. The type signatures for all functions have the patch type as their first type variable. For 'diff' \/ 'diffNoEq' \/ 'diffWith', this allows setting the return type with a single type application. -} module Data.Semialign.Diff ( -- * Diffing diff , diffNoEq , diffWith -- * Patching , patch , patchWith ) where import Control.Lens ( AsEmpty(..) , At(..) , pattern Empty , FoldableWithIndex(..) , Index , IxValue , set ) import Control.Lens.Operators import Data.Semialign (Semialign(..)) import Data.These (These(..)) -- $setup -- >>> :set -XTypeApplications -- >>> import Data.Map (Map, (!), fromList) -- | Diff two structures. -- -- >>> :{ -- let -- old = fromList [("Alice", 1), ("Bob", 2)] -- new = fromList [("Alice", 3), ("Carol", 4)] -- in -- diff old new :: Map String (Maybe Int) -- :} -- fromList [("Alice",Just 3),("Bob",Nothing),("Carol",Just 4)] -- -- @since 0.1.0.0 diff :: forall p f i a . ( FoldableWithIndex i f , Semialign f , Eq a , AsEmpty p , At p , Index p ~ i , IxValue p ~ (Maybe a) ) => f a -> f a -> p diff = diffWith $ \case This _ -> Just Nothing That new -> Just $ Just new These old new | old == new -> Nothing | otherwise -> Just $ Just new -- | Diff two structures without requiring an 'Eq' instance. Instead, -- always assume a new value wherever the structures align: -- -- >>> :{ -- let -- old = fromList [("Alice", (+ 1))] -- new = fromList [("Alice", (* 2))] -- in -- ($ 3) <$> diffNoEq old new ! "Alice" -- :} -- Just 6 -- -- @since 0.1.0.0 diffNoEq :: forall p f i a . ( FoldableWithIndex i f , Semialign f , AsEmpty p , At p , Index p ~ i , IxValue p ~ Maybe a ) => f a -> f a -> p diffNoEq = diffWith $ Just . \case This _ -> Nothing That new -> Just new These _ new -> Just new -- | Diff two structures with a custom function. -- -- This function should return 'Nothing' if there is no meaningful -- change and @'Just' new@ to indicate a changed value. -- -- Often, @c@ is itself a @'Maybe'@, to indicate deletion/replacement -- of a value. -- -- @since 0.1.0.0 diffWith :: forall p f i a b c . ( FoldableWithIndex i f , Semialign f , AsEmpty p , At p , Index p ~ i , IxValue p ~ c ) => (These a b -> Maybe c) -> f a -> f b -> p diffWith f = (ifoldr step Empty .) . align where step k = set (at k) . f -- | Apply a patch to a structure. -- -- >>> patch (fromList [(0, Just 0), (1, Just 3), (2, Nothing)]) (fromList [(0, 1), (2, 3)]) -- fromList [(0,0),(1,3)] -- -- When the types are compatible, 'patch' undoes 'diff' / 'diffNoEq': -- -- prop> \old new -> let p = diff @(Map Int (Maybe Int)) old (new :: Map Int Int) in (patch p old) == new -- prop> \old new -> let p = diffNoEq @(Map Int (Maybe Int)) old (new :: Map Int Int) in (patch p old) == new -- -- @since 0.1.0.0 patch :: forall p m i a . ( FoldableWithIndex i p , At m , Index m ~ i , IxValue m ~ a ) => p (Maybe a) -> m -> m patch = patchWith $ const id -- | Apply changes to a structure with a custom function, folding over -- the patch. -- -- The provided function receives two arguments: the old value if -- present and the new value from the patch. It should return @'Just' -- new@ to store @new@ into the result, or 'Nothing' to delete it. -- -- @since 0.1.0.0 patchWith :: forall p m i a b . ( FoldableWithIndex i p , At m , Index m ~ i , IxValue m ~ a ) => (Maybe a -> b -> Maybe a) -> p b -> m -> m patchWith f p m = ifoldr step m p where step k v = at k %~ flip f v