diff --git a/Data/Algorithm/Diff3.hs b/Data/Algorithm/Diff3.hs
deleted file mode 100644
--- a/Data/Algorithm/Diff3.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-| An implementation of a 3-way merge algorithm. -}
-module Data.Algorithm.Diff3 (Hunk(..), diff3) where
-
-import Data.Algorithm.Diff
-import Data.Monoid (Monoid, mempty, mappend)
-
---------------------------------------------------------------------------------
--- | A hunk is a collection of changes that occur in a document. A hunk can be
--- some changes only in A, only in B, in both A & B (equally), or conflicting
--- between A, B and the original document.
-data Hunk a = ChangedInA [a] | ChangedInB [a] | Both [a] | Conflict [a] [a] [a]
-  deriving (Eq, Show)
-
-
---------------------------------------------------------------------------------
--- | Perform a 3-way diff against 2 documents and the original document.
-diff3 :: (Eq a) => [a] -> [a] -> [a] -> [Hunk a]
-diff3 a o b = step (getDiff o a) (getDiff o b)
-  where
-    step :: [(DI, a)] -> [(DI, a)] -> [Hunk a]
-    step [] [] = []
-    step oa ob =
-      let (conflictHunk, ra, rb) = shortestConflict oa ob
-          (matchHunk, ra', rb')  = shortestMatch ra rb
-      in conflictHunk ++ matchHunk ++ step ra' rb'
-
-
---------------------------------------------------------------------------------
-toHunk :: [(DI, a)] -> [(DI, a)] -> [Hunk a]
-toHunk [] [] = mempty
-toHunk a  [] = return $ ChangedInA $ map snd a
-toHunk [] b  = return $ ChangedInB $ map snd b
-toHunk a  b
-  | all isB a && all isB b = return $ Both $ map snd $ filter isA a
-  | all isB a = return $ ChangedInB $ map snd $ filter isA b
-  | all isB b = return $ ChangedInA $ map snd $ filter isA a
-  | otherwise = return $ Conflict (map snd $ filter isA a)
-                                  (map snd $ filter isO a)
-                                  (map snd $ filter isA b)
-
-
---------------------------------------------------------------------------------
-isA :: (DI, t) -> Bool
-isA (F,_) = False
-isA (_,_) = True
-{-# INLINE isA #-}
-
---------------------------------------------------------------------------------
-isO :: (DI, t) -> Bool
-isO (S,_) = False
-isO (_,_) = True
-{-# INLINE isO #-}
-
-
---------------------------------------------------------------------------------
-isB :: (DI, t) -> Bool
-isB (B,_) = True
-isB (_,_) = False
-{-# INLINE isB #-}
-
---------------------------------------------------------------------------------
-shortestMatch :: [(DI,a)] -> [(DI,a)] -> ([Hunk a], [(DI, a)], [(DI, a)])
-shortestMatch oa ob = go oa ob [] []
-  where
-    go (x@(B,_):xs) (y@(B,_):ys) accX accY = go xs ys (accX ++ [x]) (accY ++ [y])
-    go xs ys accX accY = (toHunk accX accY, xs, ys)
-
-
---------------------------------------------------------------------------------
-shortestConflict :: [(DI,a)] -> [(DI,a)] -> ([Hunk a], [(DI, a)], [(DI, a)])
-shortestConflict l r =
-    let (hunk, rA, rB) = go l r
-    in (uncurry toHunk hunk, rA, rB)
-  where
-    go a b =
-      let (as, ta) = break isBoth a
-          (bs, tb) = break isBoth b
-          am = sum $ map motion as
-          bm = sum $ map motion bs
-          (as', ta') = incurMotion bm ta
-          (bs', tb') = incurMotion am tb
-      in if am == bm
-         then ((as, bs), ta, tb)
-         else ((as ++ as', bs ++ bs'), [], []) <> go ta' tb'
-
-    isBoth (B,_) = True
-    isBoth (_,_) = False
-
-    motion (S,_) = 0
-    motion _ = 1
-
-
---------------------------------------------------------------------------------
-incurMotion :: Int -> [(DI, t)] -> ([(DI,t)], [(DI,t)])
-incurMotion _ [] = ([], [])
-incurMotion 0 as  = ([], as)
-incurMotion n (a@(B,_):as) = ([a], []) <> incurMotion (pred n) as
-incurMotion n (a@(S,_):as) = ([a], []) <> incurMotion (pred n) as
-incurMotion n (a:as) = ([a], []) <> incurMotion n as
-
-
---------------------------------------------------------------------------------
--- This is here so we can build on GHC 7.4.
-infixr 6 <>
-
--- | An infix synonym for 'mappend'.
-(<>) :: Monoid m => m -> m -> m
-(<>) = mappend
-{-# INLINE (<>) #-}
diff --git a/diff3.cabal b/diff3.cabal
--- a/diff3.cabal
+++ b/diff3.cabal
@@ -1,5 +1,5 @@
 name:                diff3
-version:             0.1.0.1
+version:             0.1.2
 synopsis:            Perform a 3-way difference of documents
 homepage:            http://github.com/ocharles/diff3.git
 license:             BSD3
@@ -20,5 +20,17 @@
 library
   exposed-modules:     Data.Algorithm.Diff3
   build-depends:       base ==4.6.*, Diff ==0.1.*
+  hs-source-dirs: src
   ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2
                -fno-warn-orphans -fno-warn-unused-do-bind
+
+
+test-suite properties
+  type: exitcode-stdio-1.0
+  main-is: test/properties.hs
+  build-depends:
+    base,
+    diff3,
+    QuickCheck,
+    test-framework,
+    test-framework-quickcheck2
diff --git a/src/Data/Algorithm/Diff3.hs b/src/Data/Algorithm/Diff3.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Algorithm/Diff3.hs
@@ -0,0 +1,123 @@
+{-| An implementation of a 3-way merge algorithm. -}
+module Data.Algorithm.Diff3 (Hunk(..), diff3, merge) where
+
+import Data.Algorithm.Diff
+import Data.Monoid (Monoid, mempty, mappend)
+
+--------------------------------------------------------------------------------
+-- | A hunk is a collection of changes that occur in a document. A hunk can be
+-- some changes only in A, only in B, in both A & B (equally), or conflicting
+-- between A, B and the original document.
+data Hunk a = ChangedInA [a] | ChangedInB [a] | Both [a] | Conflict [a] [a] [a]
+  deriving (Eq, Show)
+
+
+--------------------------------------------------------------------------------
+-- | Perform a 3-way diff against 2 documents and the original document.
+diff3 :: Eq a => [a] -> [a] -> [a] -> [Hunk a]
+diff3 a o b = step (getDiff o a) (getDiff o b)
+  where
+    step [] [] = []
+    step [] ob = toHunk [] ob
+    step oa [] = toHunk oa []
+    step oa ob =
+      let (conflictHunk, ra, rb) = shortestConflict oa ob
+          (matchHunk, ra', rb')  = shortestMatch ra rb
+      in conflictHunk ++ matchHunk ++ step ra' rb'
+
+
+--------------------------------------------------------------------------------
+merge :: [Hunk a] -> Either [Hunk a] [a]
+merge hunks = maybe (Left hunks) Right $ go hunks
+  where
+    go [] = Just []
+    go ((Conflict _ _ _):_) = Nothing
+    go ((ChangedInA as):t) = fmap (as ++) $ go t
+    go ((ChangedInB bs):t) = fmap (bs ++) $ go t
+    go ((Both xs):t) = fmap (xs ++) $ go t
+
+
+--------------------------------------------------------------------------------
+toHunk :: [(DI, a)] -> [(DI, a)] -> [Hunk a]
+toHunk [] [] = mempty
+toHunk a  [] = return $ ChangedInA $ map snd a
+toHunk [] b  = return $ ChangedInB $ map snd b
+toHunk a  b
+  | all isB a && all isB b = return $ Both $ map snd $ filter isA a
+  | all isB a = return $ ChangedInB $ map snd $ filter isA b
+  | all isB b = return $ ChangedInA $ map snd $ filter isA a
+  | otherwise = return $ Conflict (map snd $ filter isA a)
+                                  (map snd $ filter isO a)
+                                  (map snd $ filter isA b)
+
+
+--------------------------------------------------------------------------------
+isA :: (DI, t) -> Bool
+isA (F,_) = False
+isA (_,_) = True
+{-# INLINE isA #-}
+
+--------------------------------------------------------------------------------
+isO :: (DI, t) -> Bool
+isO (S,_) = False
+isO (_,_) = True
+{-# INLINE isO #-}
+
+
+--------------------------------------------------------------------------------
+isB :: (DI, t) -> Bool
+isB (B,_) = True
+isB (_,_) = False
+{-# INLINE isB #-}
+
+--------------------------------------------------------------------------------
+shortestMatch :: [(DI,a)] -> [(DI,a)] -> ([Hunk a], [(DI, a)], [(DI, a)])
+shortestMatch oa ob = go oa ob [] []
+  where
+    go (x@(B,_):xs) (y@(B,_):ys) accX accY = go xs ys (accX ++ [x]) (accY ++ [y])
+    go xs ys accX accY = (toHunk accX accY, xs, ys)
+
+
+--------------------------------------------------------------------------------
+shortestConflict :: [(DI,a)] -> [(DI,a)] -> ([Hunk a], [(DI, a)], [(DI, a)])
+shortestConflict l r =
+    let (hunk, rA, rB) = go l r
+    in (uncurry toHunk hunk, rA, rB)
+  where
+    go [] b = (([], b), [], [])
+    go a [] = ((a, []), [], [])
+    go a b =
+      let (as, ta) = break isBoth a
+          (bs, tb) = break isBoth b
+          am = sum $ map motion as
+          bm = sum $ map motion bs
+          (as', ta') = incurMotion bm ta
+          (bs', tb') = incurMotion am tb
+      in if am == bm
+         then ((as, bs), ta, tb)
+         else ((as ++ as', bs ++ bs'), [], []) <> go ta' tb'
+
+    isBoth (B,_) = True
+    isBoth (_,_) = False
+
+    motion (S,_) = 0
+    motion _ = 1
+
+
+--------------------------------------------------------------------------------
+incurMotion :: Int -> [(DI, t)] -> ([(DI,t)], [(DI,t)])
+incurMotion _ [] = ([], [])
+incurMotion 0 as  = ([], as)
+incurMotion n (a@(B,_):as) = ([a], []) <> incurMotion (pred n) as
+incurMotion n (a@(S,_):as) = ([a], []) <> incurMotion (pred n) as
+incurMotion n (a:as) = ([a], []) <> incurMotion n as
+
+
+--------------------------------------------------------------------------------
+-- This is here so we can build on GHC 7.4.
+infixr 6 <>
+
+-- | An infix synonym for 'mappend'.
+(<>) :: Monoid m => m -> m -> m
+(<>) = mappend
+{-# INLINE (<>) #-}
diff --git a/test/properties.hs b/test/properties.hs
new file mode 100644
--- /dev/null
+++ b/test/properties.hs
@@ -0,0 +1,34 @@
+module Main where
+
+import Data.Algorithm.Diff3
+import Data.Monoid (mempty)
+import Test.Framework
+import Test.QuickCheck
+import Test.Framework.Providers.QuickCheck2
+
+leftChanges :: [Int] -> [Int] -> Bool
+leftChanges left original = merge (diff3 left original original) == Right left
+
+rightChanges :: [Int] -> [Int] -> Bool
+rightChanges right original = merge (diff3 original original right) == Right right
+
+identicalChanges :: [Int] -> [Int] -> Property
+identicalChanges changed original = (changed /= original) ==>
+    conflicts (merge (diff3 changed original changed))
+  where conflicts (Left _) = True
+        conflicts (Right _) = False
+
+identityMerge :: [Int] -> Bool
+identityMerge as = merge (diff3 as as as) == Right as
+
+main :: IO ()
+main =
+  let testOpts = mempty { topt_maximum_generated_tests = Just 5000 }
+      runner = mempty { ropt_test_options = Just testOpts }
+  in defaultMainWithOpts
+       [ testProperty "Can make changes in left document" leftChanges
+       , testProperty "Can make changes in right document" rightChanges
+       , testProperty "Left/right identical changes conflict" identicalChanges
+       , testProperty "The 'identity' merge always succeeds" identityMerge
+       ]
+       runner
