diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for patch
+
+## Unreleased
+
+* Extract patching functionality from Reflex.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 2015, Obsidian Systems LLC
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder nor the names of its 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 HOLDER 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+## Patchable
+
+Infrastructure for writing patches which act on other types.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/patch.cabal b/patch.cabal
new file mode 100644
--- /dev/null
+++ b/patch.cabal
@@ -0,0 +1,75 @@
+Name: patch
+Version: 0.0.0.0
+Synopsis: Infrastructure for writing patches which act on other types.
+License: BSD3
+License-file: LICENSE
+Author: Ryan Trinkle
+Maintainer: maintainer@obsidian.systems
+Stability: Experimental
+Category: FRP
+Build-type: Simple
+Cabal-version: >=1.9.2
+homepage: https://obsidian.systems
+bug-reports: https://github.com/reflex-frp/patch/issues
+extra-source-files:
+  README.md
+  ChangeLog.md
+
+tested-with:
+  GHC  ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.1
+  GHCJS ==8.4
+
+library
+  hs-source-dirs: src
+  build-depends: base >= 4.9 && < 4.14
+               , bifunctors >= 5.2 && < 5.6
+               , comonad >= 5.0.4 && < 5.1
+               , constraints-extras >= 0.3 && < 0.4
+               , containers >= 0.6 && < 0.7
+               , dependent-map >= 0.3 && < 0.4
+               , dependent-sum >= 0.6 && < 0.7
+               , lens >= 4.7 && < 5
+               , monoidal-containers >= 0.6 && < 0.7
+               , mtl >= 2.1 && < 2.3
+               , prim-uniq >= 0.1.0.1 && < 0.2
+               , primitive >= 0.5 && < 0.8
+               , profunctors >= 5.3 && < 5.6
+               , random == 1.1.*
+               , ref-tf == 0.4.*
+               , reflection == 2.1.*
+               , semialign >=1 && <1.2
+               , semigroupoids >= 4.0 && < 6
+               , stm >= 2.4 && < 2.6
+               , syb >= 0.5 && < 0.8
+               , these >= 1 && <1.1
+               , time >= 1.4 && < 1.10
+               , transformers >= 0.5.6.0 && < 0.6
+               , unbounded-delays >= 0.1.0.0 && < 0.2
+               , witherable >= 0.3 && < 0.3.2
+
+  exposed-modules: Data.Functor.Misc
+                 , Data.Patch
+                 , Data.Patch.Class
+                 , Data.Patch.DMap
+                 , Data.Patch.DMapWithMove
+                 , Data.Patch.IntMap
+                 , Data.Patch.Map
+                 , Data.Patch.MapWithMove
+
+  ghc-options: -Wall -fwarn-redundant-constraints -fwarn-tabs
+
+test-suite hlint
+  type: exitcode-stdio-1.0
+  main-is: hlint.hs
+  hs-source-dirs: test
+  build-depends: base
+               , directory
+               , filepath
+               , filemanip
+               , hlint < 2.1 || >= 2.2.2
+  if impl(ghcjs)
+    buildable: False
+
+source-repository head
+  type: git
+  location: https://github.com/reflex-frp/patch
diff --git a/src/Data/Functor/Misc.hs b/src/Data/Functor/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Functor/Misc.hs
@@ -0,0 +1,232 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+-- | This module provides types and functions with no particular theme, but
+-- which are relevant to the use of 'Functor'-based datastructures like
+-- 'Data.Dependent.Map.DMap'.
+module Data.Functor.Misc
+  ( -- * Const2
+    Const2 (..)
+  , unConst2
+  , dmapToMap
+  , dmapToIntMap
+  , dmapToMapWith
+  , mapToDMap
+  , weakenDMapWith
+    -- * WrapArg
+  , WrapArg (..)
+    -- * Convenience functions for DMap
+  , mapWithFunctorToDMap
+  , intMapWithFunctorToDMap
+  , mapKeyValuePairsMonotonic
+  , combineDMapsWithKey
+  , EitherTag (..)
+  , dmapToThese
+  , eitherToDSum
+  , dsumToEither
+  , ComposeMaybe (..)
+  ) where
+
+import Control.Applicative ((<$>))
+import Control.Monad.Identity
+import Data.Dependent.Map (DMap)
+import qualified Data.Dependent.Map as DMap
+import Data.Dependent.Sum
+import Data.GADT.Compare
+import Data.GADT.Show
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IntMap
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Some (Some(Some))
+import Data.These
+import Data.Type.Equality ((:~:)(Refl))
+import Data.Typeable hiding (Refl)
+
+--------------------------------------------------------------------------------
+-- Const2
+--------------------------------------------------------------------------------
+
+-- | 'Const2' stores a value of a given type 'k' and ensures that a particular
+-- type 'v' is always given for the last type parameter
+data Const2 :: * -> x -> x -> * where
+  Const2 :: k -> Const2 k v v
+  deriving (Typeable)
+
+-- | Extract the value from a Const2
+unConst2 :: Const2 k v v' -> k
+unConst2 (Const2 k) = k
+
+deriving instance Eq k => Eq (Const2 k v v')
+deriving instance Ord k => Ord (Const2 k v v')
+deriving instance Show k => Show (Const2 k v v')
+deriving instance Read k => Read (Const2 k v v)
+
+instance Show k => GShow (Const2 k v) where
+  gshowsPrec n x@(Const2 _) = showsPrec n x
+
+instance Eq k => GEq (Const2 k v) where
+  geq (Const2 a) (Const2 b) =
+    if a == b
+    then Just Refl
+    else Nothing
+
+instance Ord k => GCompare (Const2 k v) where
+  gcompare (Const2 a) (Const2 b) = case compare a b of
+    LT -> GLT
+    EQ -> GEQ
+    GT -> GGT
+
+-- | Convert a 'DMap' to a regular 'Map'
+dmapToMap :: DMap (Const2 k v) Identity -> Map k v
+dmapToMap = Map.fromDistinctAscList . map (\(Const2 k :=> Identity v) -> (k, v)) . DMap.toAscList
+
+-- | Convert a 'DMap' to an 'IntMap'
+dmapToIntMap :: DMap (Const2 IntMap.Key v) Identity -> IntMap v
+dmapToIntMap = IntMap.fromDistinctAscList . map (\(Const2 k :=> Identity v) -> (k, v)) . DMap.toAscList
+
+-- | Convert a 'DMap' to a regular 'Map', applying the given function to remove
+-- the wrapping 'Functor'
+dmapToMapWith :: (f v -> v') -> DMap (Const2 k v) f -> Map k v'
+dmapToMapWith f = Map.fromDistinctAscList . map (\(Const2 k :=> v) -> (k, f v)) . DMap.toAscList
+
+-- | Convert a regular 'Map' to a 'DMap'
+mapToDMap :: Map k v -> DMap (Const2 k v) Identity
+mapToDMap = DMap.fromDistinctAscList . map (\(k, v) -> Const2 k :=> Identity v) . Map.toAscList
+
+-- | Convert a regular 'Map', where the values are already wrapped in a functor,
+-- to a 'DMap'
+mapWithFunctorToDMap :: Map k (f v) -> DMap (Const2 k v) f
+mapWithFunctorToDMap = DMap.fromDistinctAscList . map (\(k, v) -> Const2 k :=> v) . Map.toAscList
+
+-- | Convert a regular 'IntMap', where the values are already wrapped in a
+-- functor, to a 'DMap'
+intMapWithFunctorToDMap :: IntMap (f v) -> DMap (Const2 IntMap.Key v) f
+intMapWithFunctorToDMap = DMap.fromDistinctAscList . map (\(k, v) -> Const2 k :=> v) . IntMap.toAscList
+
+-- | Convert a 'DMap' to a regular 'Map' by forgetting the types associated with
+-- the keys, using a function to remove the wrapping 'Functor'
+weakenDMapWith :: (forall a. v a -> v') -> DMap k v -> Map (Some k) v'
+weakenDMapWith f = Map.fromDistinctAscList . map (\(k :=> v) -> (Some k, f v)) . DMap.toAscList
+
+--------------------------------------------------------------------------------
+-- WrapArg
+--------------------------------------------------------------------------------
+
+-- | 'WrapArg' can be used to tag a value in one functor with a type
+-- representing another functor.  This was primarily used with dependent-map <
+-- 0.2, in which the value type was not wrapped in a separate functor.
+data WrapArg :: (k -> *) -> (k -> *) -> * -> * where
+  WrapArg :: f a -> WrapArg g f (g a)
+
+deriving instance Eq (f a) => Eq (WrapArg g f (g' a))
+deriving instance Ord (f a) => Ord (WrapArg g f (g' a))
+deriving instance Show (f a) => Show (WrapArg g f (g' a))
+deriving instance Read (f a) => Read (WrapArg g f (g a))
+
+instance GEq f => GEq (WrapArg g f) where
+  geq (WrapArg a) (WrapArg b) = (\Refl -> Refl) <$> geq a b
+
+instance GCompare f => GCompare (WrapArg g f) where
+  gcompare (WrapArg a) (WrapArg b) = case gcompare a b of
+    GLT -> GLT
+    GEQ -> GEQ
+    GGT -> GGT
+
+--------------------------------------------------------------------------------
+-- Convenience functions for DMap
+--------------------------------------------------------------------------------
+
+-- | Map over all key/value pairs in a 'DMap', potentially altering the key as
+-- well as the value.  The provided function MUST preserve the ordering of the
+-- keys, or the resulting 'DMap' will be malformed.
+mapKeyValuePairsMonotonic :: (DSum k v -> DSum k' v') -> DMap k v -> DMap k' v'
+mapKeyValuePairsMonotonic f = DMap.fromDistinctAscList . map f . DMap.toAscList
+
+{-# INLINE combineDMapsWithKey #-}
+-- | Union two 'DMap's of different types, yielding another type.  Each key that
+-- is present in either input map will be present in the output.
+combineDMapsWithKey :: forall f g h i.
+                       GCompare f
+                    => (forall a. f a -> These (g a) (h a) -> i a)
+                    -> DMap f g
+                    -> DMap f h
+                    -> DMap f i
+combineDMapsWithKey f mg mh = DMap.fromList $ go (DMap.toList mg) (DMap.toList mh)
+  where go :: [DSum f g] -> [DSum f h] -> [DSum f i]
+        go [] hs = map (\(hk :=> hv) -> hk :=> f hk (That hv)) hs
+        go gs [] = map (\(gk :=> gv) -> gk :=> f gk (This gv)) gs
+        go gs@((gk :=> gv) : gs') hs@((hk :=> hv) : hs') = case gk `gcompare` hk of
+          GLT -> (gk :=> f gk (This gv)) : go gs' hs
+          GEQ -> (gk :=> f gk (These gv hv)) : go gs' hs'
+          GGT -> (hk :=> f hk (That hv)) : go gs hs'
+
+-- | Extract the values of a 'DMap' of 'EitherTag's.
+dmapToThese :: DMap (EitherTag a b) Identity -> Maybe (These a b)
+dmapToThese m = case (DMap.lookup LeftTag m, DMap.lookup RightTag m) of
+  (Nothing, Nothing) -> Nothing
+  (Just (Identity a), Nothing) -> Just $ This a
+  (Nothing, Just (Identity b)) -> Just $ That b
+  (Just (Identity a), Just (Identity b)) -> Just $ These a b
+
+-- | Tag type for 'Either' to use it as a 'DSum'.
+data EitherTag l r a where
+  LeftTag :: EitherTag l r l
+  RightTag :: EitherTag l r r
+  deriving (Typeable)
+
+deriving instance Show (EitherTag l r a)
+deriving instance Eq (EitherTag l r a)
+deriving instance Ord (EitherTag l r a)
+
+instance GEq (EitherTag l r) where
+  geq a b = case (a, b) of
+    (LeftTag, LeftTag) -> Just Refl
+    (RightTag, RightTag) -> Just Refl
+    _ -> Nothing
+
+instance GCompare (EitherTag l r) where
+  gcompare a b = case (a, b) of
+    (LeftTag, LeftTag) -> GEQ
+    (LeftTag, RightTag) -> GLT
+    (RightTag, LeftTag) -> GGT
+    (RightTag, RightTag) -> GEQ
+
+instance GShow (EitherTag l r) where
+  gshowsPrec _ a = case a of
+    LeftTag -> showString "LeftTag"
+    RightTag -> showString "RightTag"
+
+-- | Convert 'Either' to a 'DSum'. Inverse of 'dsumToEither'.
+eitherToDSum :: Either a b -> DSum (EitherTag a b) Identity
+eitherToDSum = \case
+  Left a -> (LeftTag :=> Identity a)
+  Right b -> (RightTag :=> Identity b)
+
+-- | Convert 'DSum' to 'Either'. Inverse of 'eitherToDSum'.
+dsumToEither :: DSum (EitherTag a b) Identity -> Either a b
+dsumToEither = \case
+  (LeftTag :=> Identity a) -> Left a
+  (RightTag :=> Identity b) -> Right b
+
+--------------------------------------------------------------------------------
+-- ComposeMaybe
+--------------------------------------------------------------------------------
+
+-- | We can't use @Compose Maybe@ instead of 'ComposeMaybe', because that would
+-- make the 'f' parameter have a nominal type role.  We need f to be
+-- representational so that we can use safe 'coerce'.
+newtype ComposeMaybe f a =
+  ComposeMaybe { getComposeMaybe :: Maybe (f a) } deriving (Show, Eq, Ord)
+
+deriving instance Functor f => Functor (ComposeMaybe f)
diff --git a/src/Data/Patch.hs b/src/Data/Patch.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE TypeFamilies #-}
+-- |
+-- Module:
+--   Data.Patch
+-- Description:
+--   This module defines the 'Patch' class.
+module Data.Patch
+  ( module Data.Patch
+  , module X
+  ) where
+
+import Data.Patch.Class as X
+import Data.Patch.DMap as X hiding (getDeletions)
+import Data.Patch.DMapWithMove as X (PatchDMapWithMove, const2PatchDMapWithMoveWith, mapPatchDMapWithMove,
+                                       patchDMapWithMoveToPatchMapWithMoveWith,
+                                       traversePatchDMapWithMoveWithKey, unPatchDMapWithMove,
+                                       unsafePatchDMapWithMove, weakenPatchDMapWithMoveWith)
+import Data.Patch.IntMap as X hiding (getDeletions)
+import Data.Patch.Map as X
+import Data.Patch.MapWithMove as X (PatchMapWithMove, patchMapWithMoveNewElements,
+                                      patchMapWithMoveNewElementsMap, unPatchMapWithMove,
+                                      unsafePatchMapWithMove)
+import Data.Map.Monoidal (MonoidalMap)
+import Data.Semigroup (Semigroup (..), (<>))
+
+-- | A 'Group' is a 'Monoid' where every element has an inverse.
+class (Semigroup q, Monoid q) => Group q where
+  negateG :: q -> q
+  (~~) :: q -> q -> q
+  r ~~ s = r <> negateG s
+
+-- | An 'Additive' 'Semigroup' is one where (<>) is commutative
+class Semigroup q => Additive q where
+
+-- | The elements of an 'Additive' 'Semigroup' can be considered as patches of their own type.
+newtype AdditivePatch p = AdditivePatch { unAdditivePatch :: p }
+
+instance Additive p => Patch (AdditivePatch p) where
+  type PatchTarget (AdditivePatch p) = p
+  apply (AdditivePatch p) q = Just $ p <> q
+
+instance (Ord k, Group q) => Group (MonoidalMap k q) where
+  negateG = fmap negateG
+
+instance (Ord k, Additive q) => Additive (MonoidalMap k q)
diff --git a/src/Data/Patch/Class.hs b/src/Data/Patch/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/Class.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE TypeFamilies #-}
+-- | The interface for types which represent changes made to other types
+module Data.Patch.Class where
+
+import Control.Monad.Identity
+import Data.Maybe
+import Data.Semigroup (Semigroup(..))
+
+-- | A 'Patch' type represents a kind of change made to a datastructure.
+--
+-- If an instance of 'Patch' is also an instance of 'Semigroup', it should obey
+-- the law that @applyAlways (f <> g) == applyAlways f . applyAlways g@.
+class Patch p where
+  type PatchTarget p :: *
+  -- | Apply the patch @p a@ to the value @a@.  If no change is needed, return
+  -- 'Nothing'.
+  apply :: p -> PatchTarget p -> Maybe (PatchTarget p)
+
+-- | Apply a 'Patch'; if it does nothing, return the original value
+applyAlways :: Patch p => p -> PatchTarget p -> PatchTarget p
+applyAlways p t = fromMaybe t $ apply p t
+
+-- | 'Identity' can be used as a 'Patch' that always fully replaces the value
+instance Patch (Identity a) where
+  type PatchTarget (Identity a) = a
+  apply (Identity a) _ = Just a
+
+-- | Like '(.)', but composes functions that return patches rather than
+-- functions that return new values.  The Semigroup instance for patches must
+-- apply patches right-to-left, like '(.)'.
+composePatchFunctions :: (Patch p, Semigroup p) => (PatchTarget p -> p) -> (PatchTarget p -> p) -> PatchTarget p -> p
+composePatchFunctions g f a =
+  let fp = f a
+  in g (applyAlways fp a) <> fp
diff --git a/src/Data/Patch/DMap.hs b/src/Data/Patch/DMap.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/DMap.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE Rank2Types #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TypeFamilies #-}
+-- | 'Patch'es on 'DMap' that consist only of insertions (or overwrites) and deletions.
+module Data.Patch.DMap where
+
+import Data.Patch.Class
+import Data.Patch.IntMap
+import Data.Patch.Map
+
+import Data.Dependent.Map (DMap, DSum (..), GCompare (..))
+import qualified Data.Dependent.Map as DMap
+import Data.Functor.Constant
+import Data.Functor.Misc
+import qualified Data.IntMap as IntMap
+import qualified Data.Map as Map
+import Data.Semigroup (Semigroup (..))
+import Data.Some (Some)
+
+-- | A set of changes to a 'DMap'.  Any element may be inserted/updated or deleted.
+-- Insertions are represented as @'ComposeMaybe' (Just value)@,
+-- while deletions are represented as @'ComposeMaybe' Nothing@.
+newtype PatchDMap k v = PatchDMap { unPatchDMap :: DMap k (ComposeMaybe v) }
+
+deriving instance GCompare k => Semigroup (PatchDMap k v)
+
+deriving instance GCompare k => Monoid (PatchDMap k v)
+
+-- | Apply the insertions or deletions to a given 'DMap'.
+instance GCompare k => Patch (PatchDMap k v) where
+  type PatchTarget (PatchDMap k v) = DMap k v
+  apply (PatchDMap diff) old = Just $! insertions `DMap.union` (old `DMap.difference` deletions) --TODO: return Nothing sometimes --Note: the strict application here is critical to ensuring that incremental merges don't hold onto all their prerequisite events forever; can we make this more robust?
+    where insertions = DMap.mapMaybeWithKey (const $ getComposeMaybe) diff
+          deletions = DMap.mapMaybeWithKey (const $ nothingToJust . getComposeMaybe) diff
+          nothingToJust = \case
+            Nothing -> Just $ Constant ()
+            Just _ -> Nothing
+
+-- | Map a function @v a -> v' a@ over any inserts/updates in the given
+-- @'PatchDMap' k v@ to produce a @'PatchDMap' k v'@.
+mapPatchDMap :: (forall a. v a -> v' a) -> PatchDMap k v -> PatchDMap k v'
+mapPatchDMap f (PatchDMap p) = PatchDMap $ DMap.map (ComposeMaybe . fmap f . getComposeMaybe) p
+
+-- | Map an effectful function @v a -> f (v' a)@ over any inserts/updates in the given
+-- @'PatchDMap' k v@ to produce a @'PatchDMap' k v'@.
+traversePatchDMap :: Applicative f => (forall a. v a -> f (v' a)) -> PatchDMap k v -> f (PatchDMap k v')
+traversePatchDMap f = traversePatchDMapWithKey $ const f
+
+-- | Map an effectful function @k a -> v a -> f (v' a)@ over any inserts/updates
+-- in the given @'PatchDMap' k v@ to produce a @'PatchDMap' k v'@.
+traversePatchDMapWithKey :: Applicative m => (forall a. k a -> v a -> m (v' a)) -> PatchDMap k v -> m (PatchDMap k v')
+traversePatchDMapWithKey f (PatchDMap p) = PatchDMap <$> DMap.traverseWithKey (\k (ComposeMaybe v) -> ComposeMaybe <$> traverse (f k) v) p
+
+-- | Weaken a @'PatchDMap' k v@ to a @'PatchMap' (Some k) v'@ using a function
+-- @v a -> v'@ to weaken each value contained in the patch.
+weakenPatchDMapWith :: (forall a. v a -> v') -> PatchDMap k v -> PatchMap (Some k) v'
+weakenPatchDMapWith f (PatchDMap p) = PatchMap $ weakenDMapWith (fmap f . getComposeMaybe) p
+
+-- | Convert a weak @'PatchDMap' ('Const2' k a) v@ where the @a@ is known by way of
+-- the @Const2@ into a @'PatchMap' k v'@ using a rank 1 function @v a -> v'@.
+patchDMapToPatchMapWith :: (v a -> v') -> PatchDMap (Const2 k a) v -> PatchMap k v'
+patchDMapToPatchMapWith f (PatchDMap p) = PatchMap $ dmapToMapWith (fmap f . getComposeMaybe) p
+
+-- | Convert a @'PatchMap' k v@ into a @'PatchDMap' ('Const2' k a) v'@ using a function @v -> v' a@.
+const2PatchDMapWith :: forall k v v' a. (v -> v' a) -> PatchMap k v -> PatchDMap (Const2 k a) v'
+const2PatchDMapWith f (PatchMap p) = PatchDMap $ DMap.fromDistinctAscList $ g <$> Map.toAscList p
+  where g :: (k, Maybe v) -> DSum (Const2 k a) (ComposeMaybe v')
+        g (k, e) = Const2 k :=> ComposeMaybe (f <$> e)
+
+-- | Convert a @'PatchIntMap' v@ into a @'PatchDMap' ('Const2' Int a) v'@ using a function @v -> v' a@.
+const2IntPatchDMapWith :: forall v f a. (v -> f a) -> PatchIntMap v -> PatchDMap (Const2 IntMap.Key a) f
+const2IntPatchDMapWith f (PatchIntMap p) = PatchDMap $ DMap.fromDistinctAscList $ g <$> IntMap.toAscList p
+  where g :: (IntMap.Key, Maybe v) -> DSum (Const2 IntMap.Key a) (ComposeMaybe f)
+        g (k, e) = Const2 k :=> ComposeMaybe (f <$> e)
+
+-- | Get the values that will be replaced or deleted if the given patch is applied to the given 'DMap'.
+getDeletions :: GCompare k => PatchDMap k v -> DMap k v' -> DMap k v'
+getDeletions (PatchDMap p) m = DMap.intersectionWithKey (\_ v _ -> v) m p
diff --git a/src/Data/Patch/DMapWithMove.hs b/src/Data/Patch/DMapWithMove.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/DMapWithMove.hs
@@ -0,0 +1,363 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE Rank2Types #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- |Module containing @'PatchDMapWithMove' k v@ and associated functions, which represents a 'Patch' to a @'DMap' k v@ which can insert, update, delete, and
+-- move values between keys.
+module Data.Patch.DMapWithMove where
+
+import Data.Patch.Class
+import Data.Patch.MapWithMove (PatchMapWithMove (..))
+import qualified Data.Patch.MapWithMove as MapWithMove
+
+import Data.Constraint.Extras
+import Data.Dependent.Map (DMap, DSum (..), GCompare (..))
+import qualified Data.Dependent.Map as DMap
+import Data.Functor.Constant
+import Data.Functor.Misc
+import Data.Functor.Product
+import Data.GADT.Compare (GEq (..))
+import Data.GADT.Show (GShow, gshow)
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Semigroup (Semigroup (..), (<>))
+import Data.Some (Some(Some))
+import Data.These
+
+-- | Like 'PatchMapWithMove', but for 'DMap'. Each key carries a 'NodeInfo' which describes how it will be changed by the patch and connects move sources and
+-- destinations.
+--
+-- Invariants:
+--
+--     * A key should not move to itself.
+--     * A move should always be represented with both the destination key (as a 'From_Move') and the source key (as a @'ComposeMaybe' ('Just' destination)@)
+newtype PatchDMapWithMove k v = PatchDMapWithMove (DMap k (NodeInfo k v))
+
+-- |Structure which represents what changes apply to a particular key. @_nodeInfo_from@ specifies what happens to this key, and in particular what other key
+-- the current key is moving from, while @_nodeInfo_to@ specifies what key the current key is moving to if involved in a move.
+data NodeInfo k v a = NodeInfo
+  { _nodeInfo_from :: !(From k v a)
+  -- ^Change applying to the current key, be it an insert, move, or delete.
+  , _nodeInfo_to :: !(To k a)
+  -- ^Where this key is moving to, if involved in a move. Should only be @ComposeMaybe (Just k)@ when there is a corresponding 'From_Move'.
+  }
+  deriving (Show)
+
+-- |Structure describing a particular change to a key, be it inserting a new key (@From_Insert@), updating an existing key (@From_Insert@ again), deleting a
+-- key (@From_Delete@), or moving a key (@From_Move@).
+data From (k :: a -> *) (v :: a -> *) :: a -> * where
+  -- |Insert a new or update an existing key with the given value @v a@
+  From_Insert :: v a -> From k v a
+  -- |Delete the existing key
+  From_Delete :: From k v a
+  -- |Move the value from the given key @k a@ to this key. The source key should also have an entry in the patch giving the current key as @_nodeInfo_to@,
+  -- usually but not necessarily with @From_Delete@.
+  From_Move :: !(k a) -> From k v a
+  deriving (Show, Read, Eq, Ord)
+
+-- |Type alias for the "to" part of a 'NodeInfo'. @'ComposeMaybe' ('Just' k)@ means the key is moving to another key, @ComposeMaybe Nothing@ for any other
+-- operation.
+type To = ComposeMaybe
+
+-- |Test whether a 'PatchDMapWithMove' satisfies its invariants.
+validPatchDMapWithMove :: forall k v. (GCompare k, GShow k) => DMap k (NodeInfo k v) -> Bool
+validPatchDMapWithMove = not . null . validationErrorsForPatchDMapWithMove
+
+-- |Enumerate what reasons a 'PatchDMapWithMove' doesn't satisfy its invariants, returning @[]@ if it's valid.
+validationErrorsForPatchDMapWithMove :: forall k v. (GCompare k, GShow k) => DMap k (NodeInfo k v) -> [String]
+validationErrorsForPatchDMapWithMove m =
+  noSelfMoves <> movesBalanced
+  where
+    noSelfMoves = mapMaybe selfMove . DMap.toAscList $ m
+    selfMove (dst :=> NodeInfo (From_Move src) _)           | Just _ <- dst `geq` src = Just $ "self move of key " <> gshow src <> " at destination side"
+    selfMove (src :=> NodeInfo _ (ComposeMaybe (Just dst))) | Just _ <- src `geq` dst = Just $ "self move of key " <> gshow dst <> " at source side"
+    selfMove _ = Nothing
+    movesBalanced = mapMaybe unbalancedMove . DMap.toAscList $ m
+    unbalancedMove (dst :=> NodeInfo (From_Move src) _) =
+      case DMap.lookup src m of
+        Nothing -> Just $ "unbalanced move at destination key " <> gshow dst <> " supposedly from " <> gshow src <> " but source key is not in the patch"
+        Just (NodeInfo _ (ComposeMaybe (Just dst'))) ->
+          if isNothing (dst' `geq` dst)
+            then Just $ "unbalanced move at destination key " <> gshow dst <> " from " <> gshow src <> " is going to " <> gshow dst' <> " instead"
+            else Nothing
+        _ ->
+          Just $ "unbalanced move at destination key " <> gshow dst <> " supposedly from " <> gshow src <> " but source key has no move to key"
+    unbalancedMove (src :=> NodeInfo _ (ComposeMaybe (Just dst))) =
+      case DMap.lookup dst m of
+        Nothing -> Just $ " unbalanced move at source key " <> gshow src <> " supposedly going to " <> gshow dst <> " but destination key is not in the patch"
+        Just (NodeInfo (From_Move src') _) ->
+          if isNothing (src' `geq` src)
+            then Just $ "unbalanced move at source key " <> gshow src <> " to " <> gshow dst <> " is coming from " <> gshow src' <> " instead"
+            else Nothing
+
+        _ ->
+          Just $ "unbalanced move at source key " <> gshow src <> " supposedly going to " <> gshow dst <> " but destination key is not moving"
+    unbalancedMove _ = Nothing
+
+-- |Test whether two @'PatchDMapWithMove' k v@ contain the same patch operations.
+instance (GEq k, Has' Eq k (NodeInfo k v)) => Eq (PatchDMapWithMove k v) where
+    PatchDMapWithMove a == PatchDMapWithMove b = a == b
+
+-- |Higher kinded 2-tuple, identical to @Data.Functor.Product@ from base ≥ 4.9
+data Pair1 f g a = Pair1 (f a) (g a)
+
+-- |Helper data structure used for composing patches using the monoid instance.
+data Fixup k v a
+   = Fixup_Delete
+   | Fixup_Update (These (From k v a) (To k a))
+
+-- |Compose patches having the same effect as applying the patches in turn: @'applyAlways' (p <> q) == 'applyAlways' p . 'applyAlways' q@
+instance GCompare k => Semigroup (PatchDMapWithMove k v) where
+  PatchDMapWithMove ma <> PatchDMapWithMove mb = PatchDMapWithMove m
+    where
+      connections = DMap.toList $ DMap.intersectionWithKey (\_ a b -> Pair1 (_nodeInfo_to a) (_nodeInfo_from b)) ma mb
+      h :: DSum k (Pair1 (ComposeMaybe k) (From k v)) -> [DSum k (Fixup k v)]
+      h (_ :=> Pair1 (ComposeMaybe mToAfter) editBefore) = case (mToAfter, editBefore) of
+        (Just toAfter, From_Move fromBefore)
+          | isJust $ fromBefore `geq` toAfter
+            -> [toAfter :=> Fixup_Delete]
+          | otherwise
+            -> [ toAfter :=> Fixup_Update (This editBefore)
+               , fromBefore :=> Fixup_Update (That (ComposeMaybe mToAfter))
+               ]
+        (Nothing, From_Move fromBefore) -> [fromBefore :=> Fixup_Update (That (ComposeMaybe mToAfter))] -- The item is destroyed in the second patch, so indicate that it is destroyed in the source map
+        (Just toAfter, _) -> [toAfter :=> Fixup_Update (This editBefore)]
+        (Nothing, _) -> []
+      mergeFixups _ Fixup_Delete Fixup_Delete = Fixup_Delete
+      mergeFixups _ (Fixup_Update a) (Fixup_Update b)
+        | This x <- a, That y <- b
+        = Fixup_Update $ These x y
+        | That y <- a, This x <- b
+        = Fixup_Update $ These x y
+      mergeFixups _ _ _ = error "PatchDMapWithMove: incompatible fixups"
+      fixups = DMap.fromListWithKey mergeFixups $ concatMap h connections
+      combineNodeInfos _ nia nib = NodeInfo
+        { _nodeInfo_from = _nodeInfo_from nia
+        , _nodeInfo_to = _nodeInfo_to nib
+        }
+      applyFixup _ ni = \case
+        Fixup_Delete -> Nothing
+        Fixup_Update u -> Just $ NodeInfo
+          { _nodeInfo_from = fromMaybe (_nodeInfo_from ni) $ getHere u
+          , _nodeInfo_to = fromMaybe (_nodeInfo_to ni) $ getThere u
+          }
+      m = DMap.differenceWithKey applyFixup (DMap.unionWithKey combineNodeInfos ma mb) fixups
+      getHere :: These a b -> Maybe a
+      getHere = \case
+        This a -> Just a
+        These a _ -> Just a
+        That _ -> Nothing
+      getThere :: These a b -> Maybe b
+      getThere = \case
+        This _ -> Nothing
+        These _ b -> Just b
+        That b -> Just b
+
+-- |Compose patches having the same effect as applying the patches in turn: @'applyAlways' (p <> q) == 'applyAlways' p . 'applyAlways' q@
+instance GCompare k => Monoid (PatchDMapWithMove k v) where
+  mempty = PatchDMapWithMove mempty
+  mappend = (<>)
+
+{-
+mappendPatchDMapWithMoveSlow :: forall k v. (ShowTag k v, GCompare k) => PatchDMapWithMove k v -> PatchDMapWithMove k v -> PatchDMapWithMove k v
+PatchDMapWithMove dstAfter srcAfter `mappendPatchDMapWithMoveSlow` PatchDMapWithMove dstBefore srcBefore = PatchDMapWithMove dst src
+  where
+    getDstAction k m = fromMaybe (From_Move k) $ DMap.lookup k m -- Any key that isn't present is treated as that key moving to itself
+    removeRedundantDst toKey (From_Move fromKey) | isJust (toKey `geq` fromKey) = Nothing
+    removeRedundantDst _ a = Just a
+    f :: forall a. k a -> From k v a -> Maybe (From k v a)
+    f toKey _ = removeRedundantDst toKey $ case getDstAction toKey dstAfter of
+      From_Move fromKey -> getDstAction fromKey dstBefore
+      nonMove -> nonMove
+    dst = DMap.mapMaybeWithKey f $ DMap.union dstAfter dstBefore
+    getSrcAction k m = fromMaybe (ComposeMaybe $ Just k) $ DMap.lookup k m
+    removeRedundantSrc fromKey (ComposeMaybe (Just toKey)) | isJust (fromKey `geq` toKey) = Nothing
+    removeRedundantSrc _ a = Just a
+    g :: forall a. k a -> ComposeMaybe k a -> Maybe (ComposeMaybe k a)
+    g fromKey _ = removeRedundantSrc fromKey $ case getSrcAction fromKey srcBefore of
+      ComposeMaybe Nothing -> ComposeMaybe Nothing
+      ComposeMaybe (Just toKeyBefore) -> getSrcAction toKeyBefore srcAfter
+    src = DMap.mapMaybeWithKey g $ DMap.union srcAfter srcBefore
+-}
+
+-- |Make a @'PatchDMapWithMove' k v@ which has the effect of inserting or updating a value @v a@ to the given key @k a@, like 'DMap.insert'.
+insertDMapKey :: k a -> v a -> PatchDMapWithMove k v
+insertDMapKey k v =
+  PatchDMapWithMove . DMap.singleton k $ NodeInfo (From_Insert v) (ComposeMaybe Nothing)
+
+-- |Make a @'PatchDMapWithMove' k v@ which has the effect of moving the value from the first key @k a@ to the second key @k a@, equivalent to:
+--
+-- @
+--     'DMap.delete' src (maybe dmap ('DMap.insert' dst) (DMap.lookup src dmap))
+-- @
+moveDMapKey :: GCompare k => k a -> k a -> PatchDMapWithMove k v
+moveDMapKey src dst = case src `geq` dst of
+  Nothing -> PatchDMapWithMove $ DMap.fromList
+    [ dst :=> NodeInfo (From_Move src) (ComposeMaybe Nothing)
+    , src :=> NodeInfo From_Delete (ComposeMaybe $ Just dst)
+    ]
+  Just _ -> mempty
+
+-- |Make a @'PatchDMapWithMove' k v@ which has the effect of swapping two keys in the mapping, equivalent to:
+--
+-- @
+--     let aMay = DMap.lookup a dmap
+--         bMay = DMap.lookup b dmap
+--     in maybe id (DMap.insert a) (bMay `mplus` aMay)
+--      . maybe id (DMap.insert b) (aMay `mplus` bMay)
+--      . DMap.delete a . DMap.delete b $ dmap
+-- @
+swapDMapKey :: GCompare k => k a -> k a -> PatchDMapWithMove k v
+swapDMapKey src dst = case src `geq` dst of
+  Nothing -> PatchDMapWithMove $ DMap.fromList
+    [ dst :=> NodeInfo (From_Move src) (ComposeMaybe $ Just src)
+    , src :=> NodeInfo (From_Move dst) (ComposeMaybe $ Just dst)
+    ]
+  Just _ -> mempty
+
+-- |Make a @'PatchDMapWithMove' k v@ which has the effect of deleting a key in the mapping, equivalent to 'DMap.delete'.
+deleteDMapKey :: k a -> PatchDMapWithMove k v
+deleteDMapKey k = PatchDMapWithMove $ DMap.singleton k $ NodeInfo From_Delete $ ComposeMaybe Nothing
+
+{-
+k1, k2 :: Const2 Int () ()
+k1 = Const2 1
+k2 = Const2 2
+p1, p2 :: PatchDMapWithMove (Const2 Int ()) Identity
+p1 = moveDMapKey k1 k2
+p2 = moveDMapKey k2 k1
+p12 = p1 <> p2
+p21 = p2 <> p1
+p12Slow = p1 `mappendPatchDMapWithMoveSlow` p2
+p21Slow = p2 `mappendPatchDMapWithMoveSlow` p1
+
+testPatchDMapWithMove = do
+  print p1
+  print p2
+  print $ p12 == deleteDMapKey k1
+  print $ p21 == deleteDMapKey k2
+  print $ p12Slow == deleteDMapKey k1
+  print $ p21Slow == deleteDMapKey k2
+
+dst (PatchDMapWithMove x _) = x
+src (PatchDMapWithMove _ x) = x
+-}
+
+-- |Extract the 'DMap' representing the patch changes from the 'PatchDMapWithMove'.
+unPatchDMapWithMove :: PatchDMapWithMove k v -> DMap k (NodeInfo k v)
+unPatchDMapWithMove (PatchDMapWithMove p) = p
+
+-- |Wrap a 'DMap' representing patch changes into a 'PatchDMapWithMove', without checking any invariants.
+--
+-- __Warning:__ when using this function, you must ensure that the invariants of 'PatchDMapWithMove' are preserved; they will not be checked.
+unsafePatchDMapWithMove :: DMap k (NodeInfo k v) -> PatchDMapWithMove k v
+unsafePatchDMapWithMove = PatchDMapWithMove
+
+-- |Wrap a 'DMap' representing patch changes into a 'PatchDMapWithMove' while checking invariants. If the invariants are satisfied, @Right p@ is returned
+-- otherwise @Left errors@.
+patchDMapWithMove :: (GCompare k, GShow k) => DMap k (NodeInfo k v) -> Either [String] (PatchDMapWithMove k v)
+patchDMapWithMove dm =
+  case validationErrorsForPatchDMapWithMove dm of
+    [] -> Right $ unsafePatchDMapWithMove dm
+    errs -> Left errs
+
+-- |Map a natural transform @v -> v'@ over the given patch, transforming @'PatchDMapWithMove' k v@ into @'PatchDMapWithMove' k v'@.
+mapPatchDMapWithMove :: forall k v v'. (forall a. v a -> v' a) -> PatchDMapWithMove k v -> PatchDMapWithMove k v'
+mapPatchDMapWithMove f (PatchDMapWithMove p) = PatchDMapWithMove $
+  DMap.map (\ni -> ni { _nodeInfo_from = g $ _nodeInfo_from ni }) p
+  where g :: forall a. From k v a -> From k v' a
+        g = \case
+          From_Insert v -> From_Insert $ f v
+          From_Delete -> From_Delete
+          From_Move k -> From_Move k
+
+-- |Traverse an effectful function @forall a. v a -> m (v ' a)@ over the given patch, transforming @'PatchDMapWithMove' k v@ into @m ('PatchDMapWithMove' k v')@.
+traversePatchDMapWithMove :: forall m k v v'. Applicative m => (forall a. v a -> m (v' a)) -> PatchDMapWithMove k v -> m (PatchDMapWithMove k v')
+traversePatchDMapWithMove f = traversePatchDMapWithMoveWithKey $ const f
+
+-- |Map an effectful function @forall a. k a -> v a -> m (v ' a)@ over the given patch, transforming @'PatchDMapWithMove' k v@ into @m ('PatchDMapWithMove' k v')@.
+traversePatchDMapWithMoveWithKey :: forall m k v v'. Applicative m => (forall a. k a -> v a -> m (v' a)) -> PatchDMapWithMove k v -> m (PatchDMapWithMove k v')
+traversePatchDMapWithMoveWithKey f (PatchDMapWithMove p) = PatchDMapWithMove <$> DMap.traverseWithKey (nodeInfoMapFromM . g) p
+  where g :: forall a. k a -> From k v a -> m (From k v' a)
+        g k = \case
+          From_Insert v -> From_Insert <$> f k v
+          From_Delete -> pure From_Delete
+          From_Move fromKey -> pure $ From_Move fromKey
+
+-- |Map a function which transforms @'From' k v a@ into a @'From' k v' a@ over a @'NodeInfo' k v a@.
+nodeInfoMapFrom :: (From k v a -> From k v' a) -> NodeInfo k v a -> NodeInfo k v' a
+nodeInfoMapFrom f ni = ni { _nodeInfo_from = f $ _nodeInfo_from ni }
+
+-- |Map an effectful function which transforms @'From' k v a@ into a @f ('From' k v' a)@ over a @'NodeInfo' k v a@.
+nodeInfoMapFromM :: Functor f => (From k v a -> f (From k v' a)) -> NodeInfo k v a -> f (NodeInfo k v' a)
+nodeInfoMapFromM f ni = fmap (\result -> ni { _nodeInfo_from = result }) $ f $ _nodeInfo_from ni
+
+-- |Weaken a 'PatchDMapWithMove' to a 'PatchMapWithMove' by weakening the keys from @k a@ to @'Some' k@ and applying a given weakening function @v a -> v'@ to
+-- values.
+weakenPatchDMapWithMoveWith :: forall k v v'. (forall a. v a -> v') -> PatchDMapWithMove k v -> PatchMapWithMove (Some k) v'
+weakenPatchDMapWithMoveWith f (PatchDMapWithMove p) = PatchMapWithMove $ weakenDMapWith g p
+  where g :: forall a. NodeInfo k v a -> MapWithMove.NodeInfo (Some k) v'
+        g ni = MapWithMove.NodeInfo
+          { MapWithMove._nodeInfo_from = case _nodeInfo_from ni of
+              From_Insert v -> MapWithMove.From_Insert $ f v
+              From_Delete -> MapWithMove.From_Delete
+              From_Move k -> MapWithMove.From_Move $ Some k
+          , MapWithMove._nodeInfo_to = Some <$> getComposeMaybe (_nodeInfo_to ni)
+          }
+
+-- |"Weaken" a @'PatchDMapWithMove' (Const2 k a) v@ to a @'PatchMapWithMove' k v'@. Weaken is in scare quotes because the 'Const2' has already disabled any
+-- dependency in the typing and all points are already @a@, hence the function to map each value to @v'@ is not higher rank.
+patchDMapWithMoveToPatchMapWithMoveWith :: forall k v v' a. (v a -> v') -> PatchDMapWithMove (Const2 k a) v -> PatchMapWithMove k v'
+patchDMapWithMoveToPatchMapWithMoveWith f (PatchDMapWithMove p) = PatchMapWithMove $ dmapToMapWith g p
+  where g :: NodeInfo (Const2 k a) v a -> MapWithMove.NodeInfo k v'
+        g ni = MapWithMove.NodeInfo
+          { MapWithMove._nodeInfo_from = case _nodeInfo_from ni of
+              From_Insert v -> MapWithMove.From_Insert $ f v
+              From_Delete -> MapWithMove.From_Delete
+              From_Move (Const2 k) -> MapWithMove.From_Move k
+          , MapWithMove._nodeInfo_to = unConst2 <$> getComposeMaybe (_nodeInfo_to ni)
+          }
+
+-- |"Strengthen" a @'PatchMapWithMove' k v@ into a @'PatchDMapWithMove ('Const2' k a)@; that is, turn a non-dependently-typed patch into a dependently typed
+-- one but which always has a constant key type represented by 'Const2'. Apply the given function to each @v@ to produce a @v' a@.
+-- Completemented by 'patchDMapWithMoveToPatchMapWithMoveWith'
+const2PatchDMapWithMoveWith :: forall k v v' a. (v -> v' a) -> PatchMapWithMove k v -> PatchDMapWithMove (Const2 k a) v'
+const2PatchDMapWithMoveWith f (PatchMapWithMove p) = PatchDMapWithMove $ DMap.fromDistinctAscList $ g <$> Map.toAscList p
+  where g :: (k, MapWithMove.NodeInfo k v) -> DSum (Const2 k a) (NodeInfo (Const2 k a) v')
+        g (k, ni) = Const2 k :=> NodeInfo
+          { _nodeInfo_from = case MapWithMove._nodeInfo_from ni of
+              MapWithMove.From_Insert v -> From_Insert $ f v
+              MapWithMove.From_Delete -> From_Delete
+              MapWithMove.From_Move fromKey -> From_Move $ Const2 fromKey
+          , _nodeInfo_to = ComposeMaybe $ Const2 <$> MapWithMove._nodeInfo_to ni
+          }
+
+-- | Apply the insertions, deletions, and moves to a given 'DMap'.
+instance GCompare k => Patch (PatchDMapWithMove k v) where
+  type PatchTarget (PatchDMapWithMove k v) = DMap k v
+  apply (PatchDMapWithMove p) old = Just $! insertions `DMap.union` (old `DMap.difference` deletions) --TODO: return Nothing sometimes --Note: the strict application here is critical to ensuring that incremental merges don't hold onto all their prerequisite events forever; can we make this more robust?
+    where insertions = DMap.mapMaybeWithKey insertFunc p
+          insertFunc :: forall a. k a -> NodeInfo k v a -> Maybe (v a)
+          insertFunc _ ni = case _nodeInfo_from ni of
+            From_Insert v -> Just v
+            From_Move k -> DMap.lookup k old
+            From_Delete -> Nothing
+          deletions = DMap.mapMaybeWithKey deleteFunc p
+          deleteFunc :: forall a. k a -> NodeInfo k v a -> Maybe (Constant () a)
+          deleteFunc _ ni = case _nodeInfo_from ni of
+            From_Delete -> Just $ Constant ()
+            _ -> Nothing
+
+-- | Get the values that will be replaced, deleted, or moved if the given patch is applied to the given 'DMap'.
+getDeletionsAndMoves :: GCompare k => PatchDMapWithMove k v -> DMap k v' -> DMap k (Product v' (ComposeMaybe k))
+getDeletionsAndMoves (PatchDMapWithMove p) m = DMap.intersectionWithKey f m p
+  where f _ v ni = Pair v $ _nodeInfo_to ni
diff --git a/src/Data/Patch/IntMap.hs b/src/Data/Patch/IntMap.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/IntMap.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TypeFamilies #-}
+-- | Module containing 'PatchIntMap', a 'Patch' for 'IntMap' which allows for
+-- insert/update or delete of associations.
+module Data.Patch.IntMap where
+
+import Data.IntMap.Strict (IntMap)
+import qualified Data.IntMap.Strict as IntMap
+import Data.Maybe
+import Data.Semigroup
+import Data.Patch.Class
+
+-- | 'Patch' for 'IntMap' which represents insertion or deletion of keys in the mapping.
+-- Internally represented by 'IntMap (Maybe a)', where @Just@ means insert/update
+-- and @Nothing@ means delete.
+newtype PatchIntMap a = PatchIntMap (IntMap (Maybe a)) deriving (Functor, Foldable, Traversable, Monoid)
+
+-- | Apply the insertions or deletions to a given 'IntMap'.
+instance Patch (PatchIntMap a) where
+  type PatchTarget (PatchIntMap a) = IntMap a
+  apply (PatchIntMap p) v = if IntMap.null p then Nothing else Just $
+    let removes = IntMap.filter isNothing p
+        adds = IntMap.mapMaybe id p
+    in IntMap.union adds $ v `IntMap.difference` removes
+
+-- | @a <> b@ will apply the changes of @b@ and then apply the changes of @a@.
+-- If the same key is modified by both patches, the one on the left will take
+-- precedence.
+instance Semigroup (PatchIntMap v) where
+  PatchIntMap a <> PatchIntMap b = PatchIntMap $ a `mappend` b --TODO: Add a semigroup instance for Map
+  -- PatchMap is idempotent, so stimes n is id for every n
+  stimes = stimesIdempotentMonoid
+
+-- | Map a function @Int -> a -> b@ over all @a@s in the given @'PatchIntMap' a@
+-- (that is, all inserts/updates), producing a @PatchIntMap b@.
+mapIntMapPatchWithKey :: (Int -> a -> b) -> PatchIntMap a -> PatchIntMap b
+mapIntMapPatchWithKey f (PatchIntMap m) = PatchIntMap $ IntMap.mapWithKey (\ k mv -> f k <$> mv) m
+
+-- | Map an effectful function @Int -> a -> f b@ over all @a@s in the given @'PatchIntMap' a@
+-- (that is, all inserts/updates), producing a @f (PatchIntMap b)@.
+traverseIntMapPatchWithKey :: Applicative f => (Int -> a -> f b) -> PatchIntMap a -> f (PatchIntMap b)
+traverseIntMapPatchWithKey f (PatchIntMap m) = PatchIntMap <$> IntMap.traverseWithKey (\k mv -> traverse (f k) mv) m
+
+-- | Extract all @a@s inserted/updated by the given @'PatchIntMap' a@.
+patchIntMapNewElements :: PatchIntMap a -> [a]
+patchIntMapNewElements (PatchIntMap m) = catMaybes $ IntMap.elems m
+
+-- | Convert the given @'PatchIntMap' a@ into an @'IntMap' a@ with all
+-- the inserts/updates in the given patch.
+patchIntMapNewElementsMap :: PatchIntMap a -> IntMap a
+patchIntMapNewElementsMap (PatchIntMap m) = IntMap.mapMaybe id m
+
+-- | Subset the given @'IntMap' a@ to contain only the keys that would be
+-- deleted by the given @'PatchIntMap' a@.
+getDeletions :: PatchIntMap v -> IntMap v' -> IntMap v'
+getDeletions (PatchIntMap m) v = IntMap.intersection v m
diff --git a/src/Data/Patch/Map.hs b/src/Data/Patch/Map.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/Map.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TypeFamilies #-}
+-- | 'Patch'es on 'Map' that consist only of insertions (including overwrites)
+-- and deletions
+module Data.Patch.Map where
+
+import Data.Patch.Class
+
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Semigroup
+
+-- | A set of changes to a 'Map'.  Any element may be inserted/updated or
+-- deleted.  Insertions are represented as values wrapped in 'Just', while
+-- deletions are represented as 'Nothing's
+newtype PatchMap k v = PatchMap { unPatchMap :: Map k (Maybe v) }
+  deriving (Show, Read, Eq, Ord)
+
+-- | Apply the insertions or deletions to a given 'Map'.
+instance Ord k => Patch (PatchMap k v) where
+  type PatchTarget (PatchMap k v) = Map k v
+  {-# INLINABLE apply #-}
+  apply (PatchMap p) old = Just $! insertions `Map.union` (old `Map.difference` deletions) --TODO: return Nothing sometimes --Note: the strict application here is critical to ensuring that incremental merges don't hold onto all their prerequisite events forever; can we make this more robust?
+    where insertions = Map.mapMaybeWithKey (const id) p
+          deletions = Map.mapMaybeWithKey (const nothingToJust) p
+          nothingToJust = \case
+            Nothing -> Just ()
+            Just _ -> Nothing
+
+-- | @a <> b@ will apply the changes of @b@ and then apply the changes of @a@.
+-- If the same key is modified by both patches, the one on the left will take
+-- precedence.
+instance Ord k => Semigroup (PatchMap k v) where
+  PatchMap a <> PatchMap b = PatchMap $ a `mappend` b --TODO: Add a semigroup instance for Map
+  -- PatchMap is idempotent, so stimes n is id for every n
+  stimes = stimesIdempotentMonoid
+
+-- | The empty 'PatchMap' contains no insertions or deletions
+instance Ord k => Monoid (PatchMap k v) where
+  mempty = PatchMap mempty
+  mappend = (<>)
+
+-- | 'fmap'ping a 'PatchMap' will alter all of the values it will insert.
+-- Deletions are unaffected.
+instance Functor (PatchMap k) where
+  fmap f = PatchMap . fmap (fmap f) . unPatchMap
+
+-- | Returns all the new elements that will be added to the 'Map'
+patchMapNewElements :: PatchMap k v -> [v]
+patchMapNewElements (PatchMap p) = catMaybes $ Map.elems p
+
+-- | Returns all the new elements that will be added to the 'Map'
+patchMapNewElementsMap :: PatchMap k v -> Map k v
+patchMapNewElementsMap (PatchMap p) = Map.mapMaybe id p
diff --git a/src/Data/Patch/MapWithMove.hs b/src/Data/Patch/MapWithMove.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Patch/MapWithMove.hs
@@ -0,0 +1,270 @@
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+-- | 'Patch'es on 'Map' that can insert, delete, and move values from one key to
+-- another
+module Data.Patch.MapWithMove where
+
+import Data.Patch.Class
+
+import Control.Arrow
+import Control.Monad.State
+import Data.Foldable
+import Data.Function
+import Data.List
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Semigroup (Semigroup (..), (<>))
+import qualified Data.Set as Set
+import Data.These (These(..))
+import Data.Tuple
+
+-- | Patch a DMap with additions, deletions, and moves.  Invariant: If key @k1@
+-- is coming from @From_Move k2@, then key @k2@ should be going to @Just k1@,
+-- and vice versa.  There should never be any unpaired From/To keys.
+newtype PatchMapWithMove k v = PatchMapWithMove (Map k (NodeInfo k v)) deriving (Show, Eq, Ord, Functor, Foldable, Traversable)
+
+-- | Holds the information about each key: where its new value should come from,
+-- and where its old value should go to
+data NodeInfo k v = NodeInfo
+  { _nodeInfo_from :: !(From k v)
+    -- ^ Where do we get the new value for this key?
+  , _nodeInfo_to :: !(To k)
+    -- ^ If the old value is being kept (i.e. moved rather than deleted or
+    -- replaced), where is it going?
+  }
+  deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable)
+
+-- | Describe how a key's new value should be produced
+data From k v
+   = From_Insert v -- ^ Insert the given value here
+   | From_Delete -- ^ Delete the existing value, if any, from here
+   | From_Move !k -- ^ Move the value here from the given key
+   deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable)
+
+-- | Describe where a key's old value will go.  If this is 'Just', that means
+-- the key's old value will be moved to the given other key; if it is 'Nothing',
+-- that means it will be deleted.
+type To = Maybe
+
+-- | Create a 'PatchMapWithMove', validating it
+patchMapWithMove :: Ord k => Map k (NodeInfo k v) -> Maybe (PatchMapWithMove k v)
+patchMapWithMove m = if valid then Just $ PatchMapWithMove m else Nothing
+  where valid = forwardLinks == backwardLinks
+        forwardLinks = Map.mapMaybe _nodeInfo_to m
+        backwardLinks = Map.fromList $ catMaybes $ flip fmap (Map.toList m) $ \(to, v) ->
+          case _nodeInfo_from v of
+            From_Move from -> Just (from, to)
+            _ -> Nothing
+
+-- | Create a 'PatchMapWithMove' that inserts everything in the given 'Map'
+patchMapWithMoveInsertAll :: Map k v -> PatchMapWithMove k v
+patchMapWithMoveInsertAll m = PatchMapWithMove $ flip fmap m $ \v -> NodeInfo
+  { _nodeInfo_from = From_Insert v
+  , _nodeInfo_to = Nothing
+  }
+
+-- | Extract the internal representation of the 'PatchMapWithMove'
+unPatchMapWithMove :: PatchMapWithMove k v -> Map k (NodeInfo k v)
+unPatchMapWithMove (PatchMapWithMove p) = p
+
+-- | Make a @'PatchMapWithMove' k v@ which has the effect of inserting or updating a value @v@ to the given key @k@, like 'Map.insert'.
+insertMapKey :: k -> v -> PatchMapWithMove k v
+insertMapKey k v = PatchMapWithMove . Map.singleton k $ NodeInfo (From_Insert v) Nothing
+
+-- |Make a @'PatchMapWithMove' k v@ which has the effect of moving the value from the first key @k@ to the second key @k@, equivalent to:
+--
+-- @
+--     'Map.delete' src (maybe map ('Map.insert' dst) (Map.lookup src map))
+-- @
+moveMapKey :: Ord k => k -> k -> PatchMapWithMove k v
+moveMapKey src dst
+  | src == dst = mempty
+  | otherwise =
+      PatchMapWithMove $ Map.fromList
+        [ (dst, NodeInfo (From_Move src) Nothing)
+        , (src, NodeInfo From_Delete (Just dst))
+        ]
+
+-- |Make a @'PatchMapWithMove' k v@ which has the effect of swapping two keys in the mapping, equivalent to:
+--
+-- @
+--     let aMay = Map.lookup a map
+--         bMay = Map.lookup b map
+--     in maybe id (Map.insert a) (bMay `mplus` aMay)
+--      . maybe id (Map.insert b) (aMay `mplus` bMay)
+--      . Map.delete a . Map.delete b $ map
+-- @
+swapMapKey :: Ord k => k -> k -> PatchMapWithMove k v
+swapMapKey src dst
+  | src == dst = mempty
+  | otherwise =
+    PatchMapWithMove $ Map.fromList
+      [ (dst, NodeInfo (From_Move src) (Just src))
+      , (src, NodeInfo (From_Move dst) (Just dst))
+      ]
+
+-- |Make a @'PatchMapWithMove' k v@ which has the effect of deleting a key in the mapping, equivalent to 'Map.delete'.
+deleteMapKey :: k -> PatchMapWithMove k v
+deleteMapKey k = PatchMapWithMove . Map.singleton k $ NodeInfo From_Delete Nothing
+
+-- | Wrap a @'Map' k (NodeInfo k v)@ representing patch changes into a @'PatchMapWithMove' k v@, without checking any invariants.
+--
+-- __Warning:__ when using this function, you must ensure that the invariants of 'PatchMapWithMove' are preserved; they will not be checked.
+unsafePatchMapWithMove :: Map k (NodeInfo k v) -> PatchMapWithMove k v
+unsafePatchMapWithMove = PatchMapWithMove
+
+-- | Apply the insertions, deletions, and moves to a given 'Map'
+instance Ord k => Patch (PatchMapWithMove k v) where
+  type PatchTarget (PatchMapWithMove k v) = Map k v
+  apply (PatchMapWithMove p) old = Just $! insertions `Map.union` (old `Map.difference` deletions) --TODO: return Nothing sometimes --Note: the strict application here is critical to ensuring that incremental merges don't hold onto all their prerequisite events forever; can we make this more robust?
+    where insertions = flip Map.mapMaybeWithKey p $ \_ ni -> case _nodeInfo_from ni of
+            From_Insert v -> Just v
+            From_Move k -> Map.lookup k old
+            From_Delete -> Nothing
+          deletions = flip Map.mapMaybeWithKey p $ \_ ni -> case _nodeInfo_from ni of
+            From_Delete -> Just ()
+            _ -> Nothing
+
+-- | Returns all the new elements that will be added to the 'Map'.
+patchMapWithMoveNewElements :: PatchMapWithMove k v -> [v]
+patchMapWithMoveNewElements = Map.elems . patchMapWithMoveNewElementsMap
+
+-- | Return a @'Map' k v@ with all the inserts/updates from the given @'PatchMapWithMove' k v@.
+patchMapWithMoveNewElementsMap :: PatchMapWithMove k v -> Map k v
+patchMapWithMoveNewElementsMap (PatchMapWithMove p) = Map.mapMaybe f p
+  where f ni = case _nodeInfo_from ni of
+          From_Insert v -> Just v
+          From_Move _ -> Nothing
+          From_Delete -> Nothing
+
+-- | Create a 'PatchMapWithMove' that, if applied to the given 'Map', will sort
+-- its values using the given ordering function.  The set keys of the 'Map' is
+-- not changed.
+patchThatSortsMapWith :: Ord k => (v -> v -> Ordering) -> Map k v -> PatchMapWithMove k v
+patchThatSortsMapWith cmp m = PatchMapWithMove $ Map.fromList $ catMaybes $ zipWith g unsorted sorted
+  where unsorted = Map.toList m
+        sorted = sortBy (cmp `on` snd) unsorted
+        f (to, _) (from, _) = if to == from then Nothing else
+          Just (from, to)
+        reverseMapping = Map.fromList $ catMaybes $ zipWith f unsorted sorted
+        g (to, _) (from, _) = if to == from then Nothing else
+          let Just movingTo = Map.lookup to reverseMapping
+          in Just (to, NodeInfo (From_Move from) $ Just movingTo)
+
+-- | Create a 'PatchMapWithMove' that, if applied to the first 'Map' provided,
+-- will produce a 'Map' with the same values as the second 'Map' but with the
+-- values sorted with the given ordering function.
+patchThatChangesAndSortsMapWith :: forall k v. (Ord k, Ord v) => (v -> v -> Ordering) -> Map k v -> Map k v -> PatchMapWithMove k v
+patchThatChangesAndSortsMapWith cmp oldByIndex newByIndexUnsorted = patchThatChangesMap oldByIndex newByIndex
+  where newList = Map.toList newByIndexUnsorted
+        newByIndex = Map.fromList $ zip (fst <$> newList) $ sortBy cmp $ snd <$> newList
+
+-- | Create a 'PatchMapWithMove' that, if applied to the first 'Map' provided,
+-- will produce the second 'Map'.
+patchThatChangesMap :: (Ord k, Ord v) => Map k v -> Map k v -> PatchMapWithMove k v
+patchThatChangesMap oldByIndex newByIndex = patch
+  where oldByValue = Map.fromListWith Set.union $ swap . first Set.singleton <$> Map.toList oldByIndex
+        (insertsAndMoves, unusedValuesByValue) = flip runState oldByValue $ do
+          let f k v = do
+                remainingValues <- get
+                let putRemainingKeys remainingKeys = put $ if Set.null remainingKeys
+                      then Map.delete v remainingValues
+                      else Map.insert v remainingKeys remainingValues
+                case Map.lookup v remainingValues of
+                  Nothing -> return $ NodeInfo (From_Insert v) $ Just undefined -- There's no existing value we can take
+                  Just fromKs ->
+                    if k `Set.member` fromKs
+                    then do
+                      putRemainingKeys $ Set.delete k fromKs
+                      return $ NodeInfo (From_Move k) $ Just undefined -- There's an existing value, and it's here, so no patch necessary
+                    else do
+                      (fromK, remainingKeys) <- return . fromJust $ Set.minView fromKs -- There's an existing value, but it's not here; move it here
+                      putRemainingKeys remainingKeys
+                      return $ NodeInfo (From_Move fromK) $ Just undefined
+          Map.traverseWithKey f newByIndex
+        unusedOldKeys = fold unusedValuesByValue
+        pointlessMove k = \case
+          From_Move k' | k == k' -> True
+          _ -> False
+        keyWasMoved k = if k `Map.member` oldByIndex && not (k `Set.member` unusedOldKeys)
+          then Just undefined
+          else Nothing
+        patch = unsafePatchMapWithMove $ Map.filterWithKey (\k -> not . pointlessMove k . _nodeInfo_from) $ Map.mergeWithKey (\k a _ -> Just $ nodeInfoSetTo (keyWasMoved k) a) (Map.mapWithKey $ \k -> nodeInfoSetTo $ keyWasMoved k) (Map.mapWithKey $ \k _ -> NodeInfo From_Delete $ keyWasMoved k) insertsAndMoves oldByIndex
+
+-- | Change the 'From' value of a 'NodeInfo'
+nodeInfoMapFrom :: (From k v -> From k v) -> NodeInfo k v -> NodeInfo k v
+nodeInfoMapFrom f ni = ni { _nodeInfo_from = f $ _nodeInfo_from ni }
+
+-- | Change the 'From' value of a 'NodeInfo', using a 'Functor' (or
+-- 'Applicative', 'Monad', etc.) action to get the new value
+nodeInfoMapMFrom :: Functor f => (From k v -> f (From k v)) -> NodeInfo k v -> f (NodeInfo k v)
+nodeInfoMapMFrom f ni = fmap (\result -> ni { _nodeInfo_from = result }) $ f $ _nodeInfo_from ni
+
+-- | Set the 'To' field of a 'NodeInfo'
+nodeInfoSetTo :: To k -> NodeInfo k v -> NodeInfo k v
+nodeInfoSetTo to ni = ni { _nodeInfo_to = to }
+
+-- |Helper data structure used for composing patches using the monoid instance.
+data Fixup k v
+   = Fixup_Delete
+   | Fixup_Update (These (From k v) (To k))
+
+-- |Compose patches having the same effect as applying the patches in turn: @'applyAlways' (p <> q) == 'applyAlways' p . 'applyAlways' q@
+instance Ord k => Semigroup (PatchMapWithMove k v) where
+  PatchMapWithMove ma <> PatchMapWithMove mb = PatchMapWithMove m
+    where
+      connections = Map.toList $ Map.intersectionWithKey (\_ a b -> (_nodeInfo_to a, _nodeInfo_from b)) ma mb
+      h :: (k, (Maybe k, From k v)) -> [(k, Fixup k v)]
+      h (_, (mToAfter, editBefore)) = case (mToAfter, editBefore) of
+        (Just toAfter, From_Move fromBefore)
+          | fromBefore == toAfter
+            -> [(toAfter, Fixup_Delete)]
+          | otherwise
+            -> [ (toAfter, Fixup_Update (This editBefore))
+               , (fromBefore, Fixup_Update (That mToAfter))
+               ]
+        (Nothing, From_Move fromBefore) -> [(fromBefore, Fixup_Update (That mToAfter))] -- The item is destroyed in the second patch, so indicate that it is destroyed in the source map
+        (Just toAfter, _) -> [(toAfter, Fixup_Update (This editBefore))]
+        (Nothing, _) -> []
+      mergeFixups _ Fixup_Delete Fixup_Delete = Fixup_Delete
+      mergeFixups _ (Fixup_Update a) (Fixup_Update b)
+        | This x <- a, That y <- b
+        = Fixup_Update $ These x y
+        | That y <- a, This x <- b
+        = Fixup_Update $ These x y
+      mergeFixups _ _ _ = error "PatchMapWithMove: incompatible fixups"
+      fixups = Map.fromListWithKey mergeFixups $ concatMap h connections
+      combineNodeInfos _ nia nib = NodeInfo
+        { _nodeInfo_from = _nodeInfo_from nia
+        , _nodeInfo_to = _nodeInfo_to nib
+        }
+      applyFixup _ ni = \case
+        Fixup_Delete -> Nothing
+        Fixup_Update u -> Just $ NodeInfo
+          { _nodeInfo_from = fromMaybe (_nodeInfo_from ni) $ getHere u
+          , _nodeInfo_to = fromMaybe (_nodeInfo_to ni) $ getThere u
+          }
+      m = Map.differenceWithKey applyFixup (Map.unionWithKey combineNodeInfos ma mb) fixups
+      getHere :: These a b -> Maybe a
+      getHere = \case
+        This a -> Just a
+        These a _ -> Just a
+        That _ -> Nothing
+      getThere :: These a b -> Maybe b
+      getThere = \case
+        This _ -> Nothing
+        These _ b -> Just b
+        That b -> Just b
+
+--TODO: Figure out how to implement this in terms of PatchDMapWithMove rather than duplicating it here
+-- |Compose patches having the same effect as applying the patches in turn: @'applyAlways' (p <> q) == 'applyAlways' p . 'applyAlways' q@
+instance Ord k => Monoid (PatchMapWithMove k v) where
+  mempty = PatchMapWithMove mempty
+  mappend = (<>)
diff --git a/test/hlint.hs b/test/hlint.hs
new file mode 100644
--- /dev/null
+++ b/test/hlint.hs
@@ -0,0 +1,38 @@
+module Main where
+
+import Control.Monad
+import Language.Haskell.HLint3 (hlint)
+import System.Directory
+import System.Exit (exitFailure, exitSuccess)
+import System.FilePath
+import System.FilePath.Find
+
+main :: IO ()
+main = do
+  pwd <- getCurrentDirectory
+  let runHlint f = hlint $ f:
+        [ "--ignore=Redundant do"
+        , "--ignore=Use camelCase"
+        , "--ignore=Redundant $"
+        , "--ignore=Use &&"
+        , "--ignore=Use &&&"
+        , "--ignore=Use const"
+        , "--ignore=Use >=>"
+        , "--ignore=Use ."
+        , "--ignore=Use unless"
+        , "--ignore=Reduce duplication"
+        , "--cpp-define=USE_TEMPLATE_HASKELL"
+        , "--ignore=Use tuple-section"
+        ]
+      recurseInto = and <$> sequence
+        [ fileType ==? Directory
+        , fileName /=? ".git"
+        ]
+      matchFile = and <$> sequence
+        [ extension ==? ".hs"
+        ]
+  files <- find recurseInto matchFile (pwd </> "src") --TODO: Someday fix all hints in tests, etc.
+  ideas <- fmap concat $ forM files $ \f -> do
+    putStr $ "linting file " ++ drop (length pwd + 1) f ++ "... "
+    runHlint f
+  if null ideas then exitSuccess else exitFailure
