Skip to content

Commit 6d043f1

Browse files
authored
Merge pull request #48 from reflex-frp/ryantrinkle/fix-fixups
Fix an exception that occurs when mappending certain `PatchMapWithPatchingMove`s
2 parents 12e1510 + 05c8181 commit 6d043f1

File tree

9 files changed

+127
-43
lines changed

9 files changed

+127
-43
lines changed

.github/workflows/haskell.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@ on: [push, pull_request]
55
jobs:
66
build:
77
strategy:
8+
fail-fast: false
89
matrix:
9-
ghc: ['8.0.2', '8.2.2', '8.4.4', '8.6.5', '8.8.4', '8.10.7', '9.0.1', '9.2.2']
10+
ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.7', '9.0.1', '9.2.2']
1011
os: ['ubuntu-latest', 'macos-latest']
11-
exclude:
12-
# There are some linker warnings in 802 on darwin that
13-
# cause compilation to fail
14-
# See https://github.com/NixOS/nixpkgs/issues/25139
15-
- ghc: '8.0.2'
16-
os: 'macos-latest'
1712
runs-on: ${{ matrix.os }}
1813

1914
name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
@@ -42,6 +37,7 @@ jobs:
4237
- name: Build
4338
run: cabal build --enable-tests --enable-benchmarks all
4439
- name: Run tests
45-
run: cabal test --enable-tests all
40+
# We don't run hlint tests, because different versions of hlint have different suggestions, and we don't want to worry about satisfying them all.
41+
run: cabal test --enable-tests -f-hlint all
4642
- name: Build Docs
4743
run: cabal haddock

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Revision history for patch
22

3+
## Unreleased
4+
5+
* Drop support for GHC 8.0 and 8.2. It may still be possible to use this library with those versions of GHC, but we do not guarantee or test it anymore.
6+
* Fix an issue where (<>) crashed for some `PatchMapWithPatchingMove`s.
7+
* Change `DecidablyEmpty` for `Sum` and `Product` to use `Num` and `Eq` rather than delegating to the argument type's `DecidablyEmpty` class. Since `Sum` and `Product` have `Monoid` actions and units that are inherently based on `Num`, it makes sense to have a `DecidablyEmpty` instances that inherently agree with that. Also, since `Int` and other numeric types don't have (and can't reasonably have) `DecidablyEmpty` instances, this is necessary to make them actually usable in this context.
8+
39
## 0.0.7.0 - 2022-06-23
410

511
* Use `commutative-semigroups` for `Commutative`, making `Additive` a

dep/reflex-platform/github.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
"repo": "reflex-platform",
44
"branch": "develop",
55
"private": false,
6-
"rev": "ac66356c8839d1dc16cc60887c2db5988a60e6c4",
7-
"sha256": "0zk8pf72lid6cqq4mlr1mcwh6zd5lz9i83kw519aci6mfba1afvq"
6+
"rev": "34c75631e7f2dd1409847b9df57252b96737e73a",
7+
"sha256": "1nwyybjy65b7qnb62wcm74nqfndr8prr2xsfvaianps0yzm366d0"
88
}

dep/reflex-platform/thunk.nix

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
33
if !fetchSubmodules && !private then builtins.fetchTarball {
44
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5-
} else (import <nixpkgs> {}).fetchFromGitHub {
5+
} else (import (builtins.fetchTarball {
6+
url = "https://github.com/NixOS/nixpkgs/archive/3aad50c30c826430b0270fcf8264c8c41b005403.tar.gz";
7+
sha256 = "0xwqsf08sywd23x0xvw4c4ghq0l28w2ki22h0bdn766i16z9q2gr";
8+
}) {}).fetchFromGitHub {
69
inherit owner repo rev sha256 fetchSubmodules private;
710
};
811
json = builtins.fromJSON (builtins.readFile ./github.json);

patch.cabal

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ extra-source-files:
2222
ChangeLog.md
2323

2424
tested-with:
25-
GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.7 || ==9.0.1 || ==9.2.2
25+
GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.7 || ==9.0.1 || ==9.2.2
2626
GHCJS ==8.4
2727

2828
flag split-these
2929
description: Use split these/semialign packages
3030
manual: False
3131
default: True
3232

33+
flag hlint
34+
description: Enable hlint test
35+
default: True
36+
3337
library
3438
hs-source-dirs: src
3539
default-language: Haskell2010
@@ -95,7 +99,7 @@ test-suite hlint
9599
, filepath
96100
, filemanip
97101
, hlint (< 2.1 || >= 2.2.2) && < 3.5
98-
if impl(ghcjs)
102+
if impl(ghcjs) || !flag(hlint)
99103
buildable: False
100104

101105
source-repository head

src/Data/Monoid/DecidablyEmpty.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ instance
6161
#endif
6262
=> DecidablyEmpty (Maybe a) where
6363
isEmpty = isNothing
64-
deriving instance (Num a, DecidablyEmpty a) => DecidablyEmpty (Product a)
65-
deriving instance (DecidablyEmpty a, Num a) => DecidablyEmpty (Sum a)
64+
instance (Num a, Eq a) => DecidablyEmpty (Product a) where
65+
isEmpty = (== 1)
66+
instance (Num a, Eq a) => DecidablyEmpty (Sum a) where
67+
isEmpty = (== 0)
6668
deriving instance DecidablyEmpty a => DecidablyEmpty (Dual a)
6769
instance DecidablyEmpty (First a) where
6870
isEmpty (First a) = isNothing a

src/Data/Patch/Class.hs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ module Data.Patch.Class where
1212
import Data.Functor.Identity
1313
import Data.Kind (Type)
1414
import Data.Maybe
15+
import Data.Semigroup
16+
( Sum (..)
17+
, Product (..)
1518
#if !MIN_VERSION_base(4,11,0)
16-
import Data.Semigroup (Semigroup(..))
19+
, Semigroup(..)
1720
#endif
21+
)
1822
import Data.Proxy
1923

2024
-- | A 'Patch' type represents a kind of change made to a datastructure.
@@ -41,6 +45,14 @@ instance forall (a :: Type). Patch (Proxy a) where
4145
type PatchTarget (Proxy a) = a
4246
apply ~Proxy _ = Nothing
4347

48+
instance (Num a, Eq a) => Patch (Sum a) where
49+
type PatchTarget (Sum a) = a
50+
apply (Sum a) b = if a == 0 then Nothing else Just $ a + b
51+
52+
instance (Num a, Eq a) => Patch (Product a) where
53+
type PatchTarget (Product a) = a
54+
apply (Product a) b = if a == 1 then Nothing else Just $ a * b
55+
4456
-- | Like '(.)', but composes functions that return patches rather than
4557
-- functions that return new values. The Semigroup instance for patches must
4658
-- apply patches right-to-left, like '(.)'.

src/Data/Patch/MapWithPatchingMove.hs

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Data.Patch.MapWithPatchingMove
2323
, patchMapWithPatchingMoveInsertAll
2424
, insertMapKey
2525
, moveMapKey
26+
, patchMapKey
2627
, swapMapKey
2728
, deleteMapKey
2829
, unsafePatchMapWithPatchingMove
@@ -135,6 +136,18 @@ moveMapKey src dst
135136
, (src, NodeInfo From_Delete (Just dst))
136137
]
137138

139+
patchMapKey
140+
:: ( DecidablyEmpty p
141+
#if !MIN_VERSION_base(4,11,0)
142+
, Semigroup p
143+
#endif
144+
)
145+
=> k -> p -> PatchMapWithPatchingMove k p
146+
patchMapKey k p
147+
| isEmpty p = PatchMapWithPatchingMove Map.empty
148+
| otherwise =
149+
PatchMapWithPatchingMove $ Map.singleton k $ NodeInfo (From_Move k p) (Just k)
150+
138151
-- |Make a @'PatchMapWithPatchingMove' k p@ which has the effect of swapping two keys in the mapping, equivalent to:
139152
--
140153
-- @
@@ -369,46 +382,78 @@ instance ( Ord k
369382
, DecidablyEmpty p
370383
, Patch p
371384
) => Semigroup (PatchMapWithPatchingMove k p) where
372-
PatchMapWithPatchingMove ma <> PatchMapWithPatchingMove mb = PatchMapWithPatchingMove m
385+
PatchMapWithPatchingMove mNew <> PatchMapWithPatchingMove mOld = PatchMapWithPatchingMove m
373386
where
374-
connections = Map.toList $ Map.intersectionWithKey (\_ a b -> (_nodeInfo_to a, _nodeInfo_from b)) ma mb
375-
h :: (k, (Maybe k, From k p)) -> [(k, Fixup k p)]
376-
h (_, (mToAfter, editBefore)) = case (mToAfter, editBefore) of
387+
connections = Map.elems $ Map.intersectionWithKey (\_ new old -> (_nodeInfo_to new, _nodeInfo_from old)) mNew mOld
388+
h :: (Maybe k, From k p) -> [(k, Fixup k p)]
389+
h = \case
377390
(Just toAfter, From_Move fromBefore p)
378391
| fromBefore == toAfter && isEmpty p
379-
-> [(toAfter, Fixup_Delete)]
392+
-> [ (toAfter, Fixup_Delete)
393+
]
380394
| otherwise
381-
-> [ (toAfter, Fixup_Update (This editBefore))
382-
, (fromBefore, Fixup_Update (That mToAfter))
395+
-> [ (toAfter, Fixup_Update (This (From_Move fromBefore p)))
396+
, (fromBefore, Fixup_Update (That (Just toAfter)))
383397
]
384-
(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
385-
(Just toAfter, _) -> [(toAfter, Fixup_Update (This editBefore))]
398+
(Nothing, From_Move fromBefore _) -> [(fromBefore, Fixup_Update (That Nothing))] -- The item is destroyed in the second patch, so indicate that it is destroyed in the source map
399+
(Just toAfter, editBefore) -> [(toAfter, Fixup_Update (This editBefore))]
386400
(Nothing, _) -> []
387-
mergeFixups _ Fixup_Delete Fixup_Delete = Fixup_Delete
388-
mergeFixups _ (Fixup_Update a) (Fixup_Update b)
401+
mergeFixups Fixup_Delete Fixup_Delete = Fixup_Delete
402+
mergeFixups (Fixup_Update a) (Fixup_Update b)
389403
| This x <- a, That y <- b
390404
= Fixup_Update $ These x y
391405
| That y <- a, This x <- b
392406
= Fixup_Update $ These x y
393-
mergeFixups _ _ _ = error "PatchMapWithPatchingMove: incompatible fixups"
394-
fixups = Map.fromListWithKey mergeFixups $ concatMap h connections
395-
combineNodeInfos _ nia nib = NodeInfo
396-
{ _nodeInfo_from = _nodeInfo_from nia
397-
, _nodeInfo_to = _nodeInfo_to nib
407+
mergeFixups _ _ = error "PatchMapWithPatchingMove: incompatible fixups"
408+
fixups = Map.fromListWithKey (\_ -> mergeFixups) $ concatMap h connections
409+
combineNodeInfos niNew niOld = NodeInfo
410+
{ _nodeInfo_from = _nodeInfo_from niNew
411+
, _nodeInfo_to = _nodeInfo_to niOld
398412
}
399-
applyFixup _ ni = \case
413+
applyFixup ni = \case
400414
Fixup_Delete -> Nothing
401415
Fixup_Update u -> Just $ NodeInfo
402416
{ _nodeInfo_from = case _nodeInfo_from ni of
403-
f@(From_Move _ p') -> case getHere u of -- The `from` fixup comes from the "old" patch
404-
Nothing -> f -- If there's no `from` fixup, just use the "new" `from`
417+
-- The new patch has a Move, so it could be affected by the
418+
-- corresponding From in the old patch. If that From exists, then
419+
-- it is in the fixup here.
420+
f@(From_Move _ p') -> case getHere u of
421+
-- If there's no `From` fixup, just use the "new" `From`
422+
Nothing -> f
423+
-- If there's a `From` fixup which is an Insert, we can just apply
424+
-- our patch to that and turn ourselves into an insert.
405425
Just (From_Insert v) -> From_Insert $ applyAlways p' v
426+
-- If there's a `From` fixup which is a Delete, then we can throw
427+
-- our patch away because there's nothing to apply it to and
428+
-- become a Delete ourselves.
406429
Just From_Delete -> From_Delete
430+
-- If there's a `From` fixup which is a Move, we need to apply
431+
-- both the old patch and the new patch (in that order) to the
432+
-- value, so we append the patches here.
407433
Just (From_Move oldKey p) -> From_Move oldKey $ p' <> p
408-
_ -> error "PatchMapWithPatchingMove: fixup for non-move From"
409-
, _nodeInfo_to = fromMaybe (_nodeInfo_to ni) $ getThere u
434+
-- If the new patch has an Insert, it doesn't care what the fixup
435+
-- value is, because it will overwrite it anyway.
436+
f@(From_Insert _) -> f
437+
-- If the new patch has an Delete, it doesn't care what the fixup
438+
-- value is, because it will overwrite it anyway.
439+
f@From_Delete -> f
440+
, _nodeInfo_to = case _nodeInfo_to ni of
441+
-- The old patch deletes this data, so we must delete it as well.
442+
-- According to the code above, any time we have this situation we
443+
-- should also have `getThere u == Nothing` because a fixup
444+
-- shouldn't be generated.
445+
Nothing -> Nothing
446+
-- The old patch sends the value to oldToAfter
447+
Just oldToAfter -> case getThere u of
448+
-- If there is no fixup, that should mean that the new patch
449+
-- doesn't do anything with the value in oldToAfter, so we still
450+
-- send it to oldToAfter
451+
Nothing -> Just oldToAfter
452+
-- If there is a fixup, it should tell us where the new patch
453+
-- sends the value at key oldToAfter. We send our value there.
454+
Just mNewToAfter -> mNewToAfter
410455
}
411-
m = Map.differenceWithKey applyFixup (Map.unionWithKey combineNodeInfos ma mb) fixups
456+
m = Map.differenceWithKey (\_ -> applyFixup) (Map.unionWith combineNodeInfos mNew mOld) fixups
412457
getHere :: These a b -> Maybe a
413458
getHere = \case
414459
This a -> Just a

test/tests.hs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE TemplateHaskell #-}
23
module Main where
34

45
import Test.HUnit (runTestTT, (~:), assertEqual, errors, failures, test)
56
import Data.Patch ( Patch(apply) )
67
import Data.Patch.MapWithMove ( patchThatChangesMap )
8+
import Data.Patch.MapWithPatchingMove (PatchMapWithPatchingMove)
9+
import qualified Data.Patch.MapWithPatchingMove as PatchMapWithPatchingMove
710
import Data.Map as Map ( Map, fromList, singleton )
811
import Hedgehog (checkParallel, discover, Property, property, forAll, PropertyT, (===))
912
import Hedgehog.Gen as Gen ( int )
1013
import Hedgehog.Range as Range ( linear )
1114
import Control.Monad (replicateM)
1215
import System.Exit (exitFailure, exitSuccess)
1316
import Data.Sequence as Seq ( foldMapWithIndex, replicateM )
17+
import Data.Semigroup
18+
( Sum (..)
19+
#if !MIN_VERSION_base(4,11,0)
20+
, Semigroup(..)
21+
#endif
22+
)
1423

1524
main :: IO ()
1625
main = do
17-
counts <- runTestTT $ test [
18-
"Simple Move" ~: (do
26+
counts <- runTestTT $ test
27+
[ "Simple Move" ~: do
1928
let mapBefore = Map.fromList [(0,1)]
2029
mapAfter = Map.fromList [(0,0),(1,1)]
2130
patch = patchThatChangesMap mapBefore mapAfter
2231
afterPatch = apply patch mapBefore
23-
assertEqual "Patch creates the same Map" (Just mapAfter) afterPatch),
24-
"Property Checks" ~: propertyChecks
25-
]
32+
assertEqual "Patch creates the same Map" (Just mapAfter) afterPatch
33+
, "Property Checks" ~: propertyChecks
34+
, "Insert and Patch" ~: do
35+
let i :: PatchMapWithPatchingMove () (Sum Int)
36+
i = PatchMapWithPatchingMove.insertMapKey () 1
37+
p = PatchMapWithPatchingMove.patchMapKey () (Sum 2)
38+
pAfterI = PatchMapWithPatchingMove.insertMapKey () 3
39+
assertEqual "Insert after patch is the same as insert" (i <> p) i
40+
assertEqual "Patch after insert is a patched insert" (p <> i) pAfterI
41+
]
2642
if errors counts + failures counts == 0 then exitSuccess else exitFailure
2743

2844
propertyChecks :: IO Bool

0 commit comments

Comments
 (0)