Skip to content

Commit 5f4c7a6

Browse files
authored
Merge pull request #52 from haskellari/omap
Add order preserving map
2 parents a8b34e9 + 5aa2c4b commit 5f4c7a6

File tree

7 files changed

+185
-25
lines changed

7 files changed

+185
-25
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Change the pretty printing to use less horizontal space.
44
`Pretty` datastructure is changed.
5+
- Change `Expr` to use `OMap`; pretty-printing preserves field order.
56
- Add `strict` and `these` instances
67
- Bump lower bounds
78

src/Data/TreeDiff/Class.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import GHC.Generics
2828
M1 (..), Selector (..), U1 (..), V1)
2929

3030
import qualified Data.Map as Map
31+
import qualified Data.TreeDiff.OMap as OMap
3132

3233
import Data.TreeDiff.Expr
3334

@@ -126,12 +127,12 @@ import Data.These (These (..))
126127
-- >>> instance ToExpr Foo
127128
--
128129
-- >>> prettyEditExpr $ ediff (Foo (Right 2) [Just True] "fo") (Foo (Right 3) [Just True] "fo")
129-
-- Foo {fooBool = [Just True], fooInt = Right -2 +3, fooString = "fo"}
130+
-- Foo {fooInt = Right -2 +3, fooBool = [Just True], fooString = "fo"}
130131
--
131132
-- >>> prettyEditExpr $ ediff (Foo (Right 42) [Just True, Just False] "old") (Foo (Right 42) [Nothing, Just False, Just True] "new")
132133
-- Foo {
133-
-- fooBool = [-Just True, +Nothing, Just False, +Just True],
134134
-- fooInt = Right 42,
135+
-- fooBool = [-Just True, +Nothing, Just False, +Just True],
135136
-- fooString = -"old" +"new"}
136137
--
137138
ediff :: ToExpr a => a -> a -> Edit EditExpr
@@ -200,7 +201,7 @@ instance (Constructor c, GProductToExpr f) => GSumToExpr (M1 i c f) where
200201
App' exprs -> App cn exprs
201202
Rec' [] -> App cn []
202203
Rec' [(_,e)] -> App cn [e]
203-
Rec' pairs -> Rec cn (Map.fromList pairs)
204+
Rec' pairs -> Rec cn (OMap.fromList pairs)
204205
where
205206
cn = conName z
206207

src/Data/TreeDiff/Expr.hs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ module Data.TreeDiff.Expr (
1212
import Prelude ()
1313
import Prelude.Compat
1414

15-
import Data.Map (Map)
15+
import Data.Semialign (alignWith)
16+
import Data.These (These (..))
17+
1618
import Data.TreeDiff.List
19+
import Data.TreeDiff.OMap (OMap)
1720

18-
import qualified Data.Map as Map
19-
import qualified Test.QuickCheck as QC
21+
import qualified Data.TreeDiff.OMap as OMap
22+
import qualified Test.QuickCheck as QC
2023

2124
-- | Constructor name is a string
2225
type ConstructorName = String
@@ -29,15 +32,15 @@ type FieldName = String
2932
-- Having richer structure than just 'Tree' allows to have richer diffs.
3033
data Expr
3134
= App ConstructorName [Expr] -- ^ application
32-
| Rec ConstructorName (Map FieldName Expr) -- ^ record constructor
35+
| Rec ConstructorName (OMap FieldName Expr) -- ^ record constructor
3336
| Lst [Expr] -- ^ list constructor
3437
deriving (Eq, Show)
3538

3639
instance QC.Arbitrary Expr where
3740
arbitrary = QC.scale (min 25) $ QC.sized arb where
3841
arb n | n <= 0 = QC.oneof
3942
[ (`App` []) <$> arbName
40-
, (`Rec` mempty) <$> arbName
43+
, (`Rec` OMap.empty) <$> arbName
4144
]
4245
| otherwise = do
4346
n' <- QC.choose (0, n `div` 3)
@@ -49,7 +52,7 @@ instance QC.Arbitrary Expr where
4952

5053
shrink (Lst es) = es
5154
++ [ Lst es' | es' <- QC.shrink es ]
52-
shrink (Rec n fs) = Map.elems fs
55+
shrink (Rec n fs) = OMap.elems fs
5356
++ [ Rec n' fs | n' <- QC.shrink n ]
5457
++ [ Rec n fs' | fs' <- QC.shrink fs ]
5558
shrink (App n es) = es
@@ -72,16 +75,22 @@ exprDiff = impl
7275
where
7376
impl ea eb | ea == eb = Cpy (EditExp ea)
7477

78+
-- application
7579
impl ea@(App a as) eb@(App b bs)
76-
| a == b = Cpy $ EditApp a (map recurse (diffBy (==) as bs))
80+
| a == b = Cpy $ EditApp a (map recurse (diffBy (==) as bs))
7781
| otherwise = Swp (EditExp ea) (EditExp eb)
82+
83+
-- records
7884
impl ea@(Rec a as) eb@(Rec b bs)
79-
| a == b = Cpy $ EditRec a $ Map.unions [inter, onlyA, onlyB]
85+
| a == b = Cpy $ EditRec a $ alignWith cls as bs
8086
| otherwise = Swp (EditExp ea) (EditExp eb)
8187
where
82-
inter = Map.intersectionWith exprDiff as bs
83-
onlyA = fmap (Del . EditExp) (Map.difference as inter)
84-
onlyB = fmap (Ins . EditExp) (Map.difference bs inter)
88+
cls :: These Expr Expr -> Edit EditExpr
89+
cls (This x) = Del (EditExp x)
90+
cls (That y) = Ins (EditExp y)
91+
cls (These x y) = exprDiff x y
92+
93+
-- lists
8594
impl (Lst as) (Lst bs) =
8695
Cpy $ EditLst (map recurse (diffBy (==) as bs))
8796

@@ -96,7 +105,7 @@ exprDiff = impl
96105
-- | Type used in the result of 'ediff'.
97106
data EditExpr
98107
= EditApp ConstructorName [Edit EditExpr]
99-
| EditRec ConstructorName (Map FieldName (Edit EditExpr))
108+
| EditRec ConstructorName (OMap FieldName (Edit EditExpr))
100109
| EditLst [Edit EditExpr]
101110
| EditExp Expr -- ^ unchanged tree
102111
deriving Show

src/Data/TreeDiff/OMap.hs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE DeriveFunctor #-}
3+
-- | Map which remembers the 'fromList' order.
4+
-- This module is minimal on purpose.
5+
module Data.TreeDiff.OMap (
6+
-- * Ordered map
7+
OMap,
8+
-- * Conversions
9+
toAscList,
10+
toList,
11+
fromList,
12+
-- * Construction
13+
empty,
14+
-- * Query
15+
elems,
16+
) where
17+
18+
import Data.List (sortBy)
19+
import Data.Ord (comparing)
20+
import Data.Semialign (Semialign (..))
21+
import Data.These (These (..))
22+
23+
#if MIN_VERSION_containers(0,5,0)
24+
import qualified Data.Map.Strict as Map
25+
#else
26+
import qualified Data.Map as Map
27+
#endif
28+
29+
import qualified Test.QuickCheck as QC
30+
31+
-- $setup
32+
-- >>> import Data.Semialign (alignWith)
33+
34+
-------------------------------------------------------------------------------
35+
-- Types
36+
-------------------------------------------------------------------------------
37+
38+
newtype OMap k v = OMap (Map.Map k (Val v))
39+
deriving (Functor)
40+
41+
-- Value with its index
42+
data Val v = Val !Int v
43+
deriving (Functor)
44+
45+
-- | Note: The instance uses 'toList', so 'Eq'ual 'OMap's can be shown differently.
46+
instance (Show k, Show v) => Show (OMap k v) where
47+
showsPrec d m = showParen (d > 10)
48+
$ showString "fromList "
49+
. showsPrec d (toList m)
50+
51+
-- |
52+
--
53+
-- >>> xs = toAscList $ fromList [('a', "alpha"), ('b', "beta"), ('g', "gamma")]
54+
-- >>> ys = toAscList $ fromList [('g', "gamma"), ('b', "beta"), ('a', "alpha")]
55+
-- >>> xs == ys
56+
-- True
57+
--
58+
-- >>> zs = toAscList $ fromList [('d', "delta"), ('b', "beta"), ('a', "alpha")]
59+
-- >>> xs == zs
60+
-- False
61+
--
62+
instance (Eq k, Eq v) => Eq (OMap k v) where
63+
xs == ys = go (toAscList xs) (toAscList ys) where
64+
go [] [] = True
65+
go _ [] = False
66+
go [] _ = False
67+
go ((k1, v1) : kvs1) ((k2, v2) : kvs2) =
68+
k1 == k2 && v1 == v2 && go kvs1 kvs2
69+
70+
-------------------------------------------------------------------------------
71+
-- QuickCheck
72+
-------------------------------------------------------------------------------
73+
74+
instance (Ord k, QC.Arbitrary k, QC.Arbitrary v) => QC.Arbitrary (OMap k v) where
75+
arbitrary = QC.arbitrary1
76+
shrink = QC.shrink1
77+
78+
instance (Ord k, QC.Arbitrary k) => QC.Arbitrary1 (OMap k) where
79+
liftArbitrary arb = fmap fromList $ QC.liftArbitrary (QC.liftArbitrary arb)
80+
liftShrink shr m = fmap fromList $ QC.liftShrink (QC.liftShrink shr) $ toList m
81+
82+
-------------------------------------------------------------------------------
83+
-- Combinators
84+
-------------------------------------------------------------------------------
85+
86+
-- |
87+
--
88+
-- >>> empty :: OMap String Integer
89+
-- fromList []
90+
--
91+
empty :: OMap k v
92+
empty = OMap Map.empty
93+
94+
-- | Elements in key ascending order.
95+
elems :: OMap k v -> [v]
96+
elems (OMap m) = map (snd . getVal) $ Map.toAscList m
97+
98+
-- |
99+
--
100+
-- >>> toAscList $ fromList [('a', "alpha"), ('b', "beta"), ('g', "gamma")]
101+
-- [('a',"alpha"),('b',"beta"),('g',"gamma")]
102+
--
103+
-- >>> toAscList $ fromList [('g', "gamma"), ('b', "beta"), ('a', "alpha")]
104+
-- [('a',"alpha"),('b',"beta"),('g',"gamma")]
105+
--
106+
toAscList :: OMap k v -> [(k, v)]
107+
toAscList (OMap m) = map getVal $ Map.toAscList m
108+
109+
-- | /O(n log n)/. List in creation order.
110+
-- Doesn't respect 'Eq' instance.
111+
--
112+
-- >>> toList $ fromList [('a', "alpha"), ('b', "beta"), ('g', "gamma")]
113+
-- [('a',"alpha"),('b',"beta"),('g',"gamma")]
114+
--
115+
-- >>> toList $ fromList [('g', "gamma"), ('b', "beta"), ('a', "alpha")]
116+
-- [('g',"gamma"),('b',"beta"),('a',"alpha")]
117+
--
118+
toList :: OMap k v -> [(k, v)]
119+
toList (OMap m) = map getVal $ sortBy (comparing getIdx) $ Map.toList m
120+
121+
getIdx :: (k, Val v) -> Int
122+
getIdx (_, Val i _) = i
123+
124+
getVal :: (k, Val v) -> (k, v)
125+
getVal (k, Val _ v) = (k, v)
126+
127+
-- |
128+
--
129+
-- >>> fromList [('g', "gamma"), ('b', "beta"), ('a', "alpha")]
130+
-- fromList [('g',"gamma"),('b',"beta"),('a',"alpha")]
131+
--
132+
fromList :: Ord k => [(k, v)] -> OMap k v
133+
fromList kvs = OMap (Map.fromList (zipWith p [0..] kvs)) where
134+
p i (k, v) = (k, Val i v)
135+
136+
-- |
137+
--
138+
-- >>> let xs = fromList [('a', "alpha"), ('b', "beta")]
139+
-- >>> let ys = fromList [('c', 3 :: Int), ('b', 2)]
140+
-- >>> alignWith id xs ys
141+
-- fromList [('a',This "alpha"),('c',That 3),('b',These "beta" 2)]
142+
--
143+
instance Ord k => Semialign (OMap k) where
144+
alignWith f (OMap xs) (OMap ys) = OMap (alignWith g xs ys) where
145+
g (This (Val i x)) = Val i (f (This x))
146+
g (That (Val j y)) = Val j (f (That y))
147+
g (These (Val i x) (Val j y)) = Val (min i j) (f (These x y))

src/Data/TreeDiff/Parser.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Text.Parser.Token.Highlight
1818

1919
import Data.TreeDiff.Expr
2020

21-
import qualified Data.Map as Map
21+
import qualified Data.TreeDiff.OMap as OMap
2222

2323
-- | Parsers for 'Expr' using @parsers@ type-classes.
2424
--
@@ -47,7 +47,7 @@ litP = atomP <|> identP <|> stringP
4747
recP :: forall m. (Monad m, TokenParsing m) => m (Either String Expr)
4848
recP = mk <$> litP <*> optional (braces (commaSep fieldP)) where
4949
mk n Nothing = Left n
50-
mk n (Just fs) = Right (Rec n (Map.fromList fs))
50+
mk n (Just fs) = Right (Rec n (OMap.fromList fs))
5151

5252
litP' :: forall m. (Monad m, TokenParsing m) => m Expr
5353
litP' = mk <$> recP <|> parens exprParser <|> lstP

src/Data/TreeDiff/Pretty.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ import Data.TreeDiff.Expr
3030
import Numeric (showHex)
3131
import Text.Read.Compat (readMaybe)
3232

33-
import qualified Data.Map as Map
33+
import qualified Data.TreeDiff.OMap as OMap
3434
import qualified Text.PrettyPrint as HJ
3535
import qualified Text.PrettyPrint.ANSI.Leijen as WL
3636

3737
-- $setup
38-
-- >>> import qualified Data.Map as Map
38+
-- >>> import qualified Data.TreeDiff.OMap as OMap
3939
-- >>> import Data.TreeDiff.Expr
4040

4141
-- | Because we don't want to commit to single pretty printing library,
@@ -118,7 +118,7 @@ ppExpr' p = impl where
118118
impl _ (App x []) = ppCon p (escapeName x)
119119
impl b (App x xs) = ppParens' b $ ppApp p (ppCon p (escapeName x)) (map (impl True) xs)
120120
impl _ (Rec x xs) = ppRec p (ppCon p (escapeName x)) $
121-
map ppField' $ Map.toList xs
121+
map ppField' $ OMap.toList xs
122122
impl _ (Lst xs) = ppLst p (map (impl False) xs)
123123

124124
ppField' (n, e) = (escapeName n, impl False e)
@@ -153,7 +153,7 @@ ppEditExpr' compact p = go
153153
ppEExpr _ (EditRec x xs) = ppRec p (ppCon p (escapeName x)) $
154154
justs ++ [ (n, ppEllip p) | n <- take 1 nothings ]
155155
where
156-
xs' = map ppField' $ Map.toList xs
156+
xs' = map ppField' $ OMap.toList xs
157157
(nothings, justs) = partitionEithers xs'
158158

159159
ppEExpr _ (EditLst xs) = ppLst p (concatMap (ppEdit False) xs)
@@ -198,7 +198,7 @@ prettyPunct sep end (x:xs) = (x HJ.<> sep) : prettyPunct sep end xs
198198

199199
-- | Pretty print 'Expr' using @pretty@.
200200
--
201-
-- >>> prettyExpr $ Rec "ex" (Map.fromList [("[]", App "bar" [])])
201+
-- >>> prettyExpr $ Rec "ex" (OMap.fromList [("[]", App "bar" [])])
202202
-- ex {`[]` = bar}
203203
prettyExpr :: Expr -> HJ.Doc
204204
prettyExpr = ppExpr prettyPretty

tree-diff.cabal

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ library
7575
Data.TreeDiff.Expr
7676
Data.TreeDiff.Golden
7777
Data.TreeDiff.List
78+
Data.TreeDiff.OMap
7879
Data.TreeDiff.Parser
7980
Data.TreeDiff.Pretty
8081
Data.TreeDiff.QuickCheck
@@ -92,14 +93,15 @@ library
9293

9394
build-depends:
9495
, aeson ^>=1.4.6.0 || ^>=1.5.6.0
95-
, ansi-terminal ^>=0.10 || ^>=0.11
96+
, ansi-terminal >=0.10 && <0.12
9697
, ansi-wl-pprint ^>=0.6.8.2
9798
, base-compat ^>=0.10.5 || ^>=0.11.0
9899
, bytestring-builder ^>=0.10.8.2.0
99100
, hashable ^>=1.2.7.0 || ^>=1.3.0.0
100101
, parsers ^>=0.12.10
101102
, QuickCheck ^>=2.14.2
102103
, scientific ^>=0.3.6.2
104+
, semialign >=1.1 && <1.3
103105
, strict ^>=0.4.0.1
104106
, tagged ^>=0.8.6
105107
, these ^>=1.1.1.1
@@ -111,10 +113,10 @@ library
111113
build-depends: ghc-prim
112114

113115
if !impl(ghc >=8.0)
114-
build-depends: semigroups >=0.19.1 && <0.20
116+
build-depends: semigroups ^>=0.19.1
115117

116118
if !impl(ghc >=7.8)
117-
build-depends: generic-deriving ^>=1.13.1 || ^>=1.14
119+
build-depends: generic-deriving >=1.13.1 && <1.15
118120

119121
if !impl(ghc >=7.10)
120122
build-depends:

0 commit comments

Comments
 (0)