packages feed

crdt 2.1 → 3.0

raw patch · 31 files changed

+632/−392 lines, 31 filesdep +QuickCheckdep +tasty-discoverPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, tasty-discover

API changes (from Hackage documentation)

- CRDT.Cm: before :: CausalOrd a => a -> a -> Bool
- CRDT.Cm: type Op u = u;
- CRDT.Cm: type View u = Payload u;
- CRDT.Cm: updateAtSource :: (CmRDT u, Clock m, Op u ~ u) => Op u -> m u
- CRDT.Cm: updateAtSourcePre :: CmRDT u => Op u -> Payload u -> Bool
- CRDT.Cm: updateDownstream :: CmRDT u => u -> Payload u -> Payload u
- CRDT.Cm: view :: (CmRDT u, Payload u ~ View u) => Payload u -> View u
- CRDT.Cm.TPSet: Add :: a -> TPSet a
- CRDT.Cm.TPSet: Remove :: a -> TPSet a
- CRDT.Cm.TPSet: data TPSet a
- CRDT.Cm.TPSet: instance GHC.Classes.Eq a => CRDT.Cm.CausalOrd (CRDT.Cm.TPSet.TPSet a)
- CRDT.Cm.TPSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cm.TPSet.TPSet a)
- CRDT.Cm.TPSet: instance GHC.Classes.Ord a => CRDT.Cm.CmRDT (CRDT.Cm.TPSet.TPSet a)
- CRDT.Cm.TPSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.TPSet.TPSet a)
- CRDT.Cm.TPSet: updateAtSource :: (CmRDT u, Clock m) => Op u -> m u
- CRDT.Cm.TPSet: updateDownstream :: CmRDT u => u -> Payload u -> Payload u
- CRDT.Cv.GSet: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Set.Base.Set a)
- CRDT.Cv.GSet: query :: Ord a => a -> GSet a -> Bool
- CRDT.Cv.Max: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Semigroup.Max a)
- CRDT.Cv.Max: point :: a -> Max a
- CRDT.LWW: Assign :: a -> Assign a
- CRDT.LWW: [timestamp] :: LWW a -> !Timestamp
- CRDT.LWW: instance Data.Semigroup.Semigroup (CRDT.LWW.LWW a)
- CRDT.LWW: instance Data.Semilattice.Semilattice (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Eq (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.LWW.Assign a)
- CRDT.LWW: instance GHC.Classes.Ord (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Show.Show a => GHC.Show.Show (CRDT.LWW.Assign a)
- CRDT.LWW: newtype Assign a
- LamportClock: Pid :: Int -> Pid
- LamportClock: Timestamp :: !Time -> !Pid -> Timestamp
- LamportClock: barrier :: [Pid] -> LamportClock ()
- LamportClock: class Applicative f => Clock f
- LamportClock: data Timestamp
- LamportClock: instance GHC.Classes.Eq LamportClock.Pid
- LamportClock: instance GHC.Classes.Eq LamportClock.Timestamp
- LamportClock: instance GHC.Classes.Ord LamportClock.Pid
- LamportClock: instance GHC.Classes.Ord LamportClock.Timestamp
- LamportClock: instance GHC.Show.Show LamportClock.Pid
- LamportClock: instance GHC.Show.Show LamportClock.Timestamp
- LamportClock: instance LamportClock.Clock LamportClock.Process
- LamportClock: newTimestamp :: Clock f => f Timestamp
- LamportClock: newtype Pid
- LamportClock: runLamportClock :: LamportClock a -> a
- LamportClock: runProcess :: Pid -> Process a -> LamportClock a
- LamportClock: type LamportClock = State LamportTime
- LamportClock: type Process = ReaderT Pid LamportClock
- LamportClock: type Time = Word
+ CRDT.Cm: affects :: CausalOrd a => a -> a -> Bool
+ CRDT.Cm: apply :: CmRDT op => op -> Payload op -> Payload op
+ CRDT.Cm: makeOp :: (CmRDT op, Intent op ~ op) => Intent op -> Payload op -> Maybe (Process op)
+ CRDT.Cm: type Intent op = op;
+ CRDT.Cm.TwoPSet: Add :: a -> TwoPSet a
+ CRDT.Cm.TwoPSet: Remove :: a -> TwoPSet a
+ CRDT.Cm.TwoPSet: data TwoPSet a
+ CRDT.Cm.TwoPSet: instance GHC.Classes.Eq a => CRDT.Cm.CausalOrd (CRDT.Cm.TwoPSet.TwoPSet a)
+ CRDT.Cm.TwoPSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cm.TwoPSet.TwoPSet a)
+ CRDT.Cm.TwoPSet: instance GHC.Classes.Ord a => CRDT.Cm.CmRDT (CRDT.Cm.TwoPSet.TwoPSet a)
+ CRDT.Cm.TwoPSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.TwoPSet.TwoPSet a)
+ CRDT.Cv.GSet: lookup :: Ord a => a -> GSet a -> Bool
+ CRDT.Cv.LwwElementSet: LES :: (Map a (LWW Bool)) -> LwwElementSet a
+ CRDT.Cv.LwwElementSet: add :: Ord a => a -> LwwElementSet a -> Process (LwwElementSet a)
+ CRDT.Cv.LwwElementSet: initial :: LwwElementSet a
+ CRDT.Cv.LwwElementSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cv.LwwElementSet.LwwElementSet a)
+ CRDT.Cv.LwwElementSet: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.Cv.LwwElementSet.LwwElementSet a)
+ CRDT.Cv.LwwElementSet: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.Cv.LwwElementSet.LwwElementSet a)
+ CRDT.Cv.LwwElementSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.LwwElementSet.LwwElementSet a)
+ CRDT.Cv.LwwElementSet: lookup :: Ord a => a -> LwwElementSet a -> Bool
+ CRDT.Cv.LwwElementSet: newtype LwwElementSet a
+ CRDT.Cv.LwwElementSet: remove :: Ord a => a -> LwwElementSet a -> Process (LwwElementSet a)
+ CRDT.Cv.Max: initial :: a -> Max a
+ CRDT.Cv.ORSet: ORSet :: (Map a (Map Tag Bool)) -> ORSet a
+ CRDT.Cv.ORSet: add :: Ord a => a -> ORSet a -> Process (ORSet a)
+ CRDT.Cv.ORSet: initial :: ORSet a
+ CRDT.Cv.ORSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cv.ORSet.ORSet a)
+ CRDT.Cv.ORSet: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.Cv.ORSet.ORSet a)
+ CRDT.Cv.ORSet: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.Cv.ORSet.ORSet a)
+ CRDT.Cv.ORSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.ORSet.ORSet a)
+ CRDT.Cv.ORSet: lookup :: Ord a => a -> ORSet a -> Bool
+ CRDT.Cv.ORSet: newtype ORSet a
+ CRDT.Cv.ORSet: remove :: Ord a => a -> ORSet a -> ORSet a
+ CRDT.Cv.TwoPSet: TwoPSet :: (Map a Bool) -> TwoPSet a
+ CRDT.Cv.TwoPSet: add :: Ord a => a -> TwoPSet a -> TwoPSet a
+ CRDT.Cv.TwoPSet: initial :: TwoPSet a
+ CRDT.Cv.TwoPSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cv.TwoPSet.TwoPSet a)
+ CRDT.Cv.TwoPSet: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.Cv.TwoPSet.TwoPSet a)
+ CRDT.Cv.TwoPSet: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.Cv.TwoPSet.TwoPSet a)
+ CRDT.Cv.TwoPSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.TwoPSet.TwoPSet a)
+ CRDT.Cv.TwoPSet: isKnown :: Ord a => a -> TwoPSet a -> Bool
+ CRDT.Cv.TwoPSet: lookup :: Ord a => a -> TwoPSet a -> Bool
+ CRDT.Cv.TwoPSet: newtype TwoPSet a
+ CRDT.Cv.TwoPSet: remove :: Ord a => a -> TwoPSet a -> TwoPSet a
+ CRDT.Cv.TwoPSet: singleton :: Ord a => a -> TwoPSet a
+ CRDT.LWW: [time] :: LWW a -> !LamportTime
+ CRDT.LWW: advanceFromLWW :: LWW a -> Process ()
+ CRDT.LWW: instance GHC.Classes.Eq a => Data.Semigroup.Semigroup (CRDT.LWW.LWW a)
+ CRDT.LWW: instance GHC.Classes.Eq a => Data.Semilattice.Semilattice (CRDT.LWW.LWW a)
+ CRDT.LWW: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.LWW.LWW a)
+ CRDT.LamportClock: LamportClock :: (State (IntMap LocalTime) a) -> LamportClock a
+ CRDT.LamportClock: LamportTime :: !LocalTime -> !Pid -> LamportTime
+ CRDT.LamportClock: Pid :: Int -> Pid
+ CRDT.LamportClock: Process :: (ReaderT Pid LamportClock a) -> Process a
+ CRDT.LamportClock: advance :: LamportTime -> Process ()
+ CRDT.LamportClock: data LamportTime
+ CRDT.LamportClock: getPid :: Process Pid
+ CRDT.LamportClock: getTime :: Process LamportTime
+ CRDT.LamportClock: instance GHC.Base.Applicative CRDT.LamportClock.LamportClock
+ CRDT.LamportClock: instance GHC.Base.Applicative CRDT.LamportClock.Process
+ CRDT.LamportClock: instance GHC.Base.Functor CRDT.LamportClock.LamportClock
+ CRDT.LamportClock: instance GHC.Base.Functor CRDT.LamportClock.Process
+ CRDT.LamportClock: instance GHC.Base.Monad CRDT.LamportClock.LamportClock
+ CRDT.LamportClock: instance GHC.Base.Monad CRDT.LamportClock.Process
+ CRDT.LamportClock: instance GHC.Classes.Eq CRDT.LamportClock.LamportTime
+ CRDT.LamportClock: instance GHC.Classes.Eq CRDT.LamportClock.Pid
+ CRDT.LamportClock: instance GHC.Classes.Ord CRDT.LamportClock.LamportTime
+ CRDT.LamportClock: instance GHC.Classes.Ord CRDT.LamportClock.Pid
+ CRDT.LamportClock: instance GHC.Show.Show CRDT.LamportClock.LamportTime
+ CRDT.LamportClock: instance GHC.Show.Show CRDT.LamportClock.Pid
+ CRDT.LamportClock: newtype LamportClock a
+ CRDT.LamportClock: newtype Pid
+ CRDT.LamportClock: newtype Process a
+ CRDT.LamportClock: runLamportClock :: LamportClock a -> a
+ CRDT.LamportClock: runProcess :: Pid -> Process a -> LamportClock a
+ Data.Semilattice: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Semigroup.Max a)
+ Data.Semilattice: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Set.Base.Set a)
- CRDT.Cm: class (CausalOrd u, Eq (View u)) => CmRDT u where type Op u type Payload u type View u type Op u = u type View u = Payload u updateAtSourcePre _ _ = True updateAtSource = pure view = id where {
+ CRDT.Cm: class (CausalOrd op, Eq (Payload op)) => CmRDT op where type Intent op type Payload op type Intent op = op makeOp i _ = Just $ pure i where {
- CRDT.Cm: type family View u;
+ CRDT.Cm: type family Payload op;
- CRDT.LWW: LWW :: !a -> !Timestamp -> LWW a
+ CRDT.LWW: LWW :: !a -> !LamportTime -> LWW a
- CRDT.LWW: assign :: Clock f => a -> LWW a -> f (LWW a)
+ CRDT.LWW: assign :: a -> LWW a -> Process (LWW a)
- CRDT.LWW: initial :: Clock f => a -> f (LWW a)
+ CRDT.LWW: initial :: a -> Process (LWW a)

Files

crdt.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack  name:           crdt-version:        2.1+version:        3.0 synopsis:       Conflict-free replicated data types description:    Definitions of CmRDT and CvRDT. Implementations for some classic CRDTs. category:       Distributed Systems@@ -31,15 +31,18 @@       CRDT.Cm       CRDT.Cm.Counter       CRDT.Cm.GSet-      CRDT.Cm.TPSet+      CRDT.Cm.TwoPSet       CRDT.Cv       CRDT.Cv.GCounter       CRDT.Cv.GSet+      CRDT.Cv.LwwElementSet       CRDT.Cv.Max+      CRDT.Cv.ORSet       CRDT.Cv.PNCounter+      CRDT.Cv.TwoPSet+      CRDT.LamportClock       CRDT.LWW       Data.Semilattice-      LamportClock   default-language: Haskell2010  test-suite test@@ -50,17 +53,24 @@   build-depends:       base >= 4.9 && < 4.11     , containers+    , mtl+    , QuickCheck     , tasty+    , tasty-discover >= 4.1     , tasty-quickcheck     , crdt   other-modules:       ArbitraryOrphans+      Cm.TwoPSet       Counter+      Cv.TwoPSet       GCounter       GSet       Laws       LWW+      LwwElementSet       Max+      ORSet       PNCounter-      TPSet+      QCUtil   default-language: Haskell2010
lib/CRDT/Cm.hs view
@@ -1,10 +1,6 @@-{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}  module CRDT.Cm     ( CausalOrd (..)@@ -12,15 +8,17 @@     , concurrent     ) where -import           LamportClock (Clock)+import           CRDT.LamportClock (Process)  -- | Partial order for causal semantics. -- Values of some type may be ordered and causally-ordered different ways. class CausalOrd a where-    before :: a -> a -> Bool+    -- | @x `affects` y@ means that+    -- @x@ must go before @y@ and @y@ can not go before @x@.+    affects :: a -> a -> Bool  comparable :: CausalOrd a => a -> a -> Bool-comparable a b = a `before` b || b `before` a+comparable a b = a `affects` b || b `affects` a  -- | Not comparable, i. e. ¬(a ≤ b) ∧ ¬(b ≤ a). concurrent :: CausalOrd a => a -> a -> Bool@@ -32,16 +30,16 @@ == Implementation  In Haskell, a CmRDT implementation consists of 3 types —-a __payload__, an __operation__ (@op@) and an __update__.+a __payload__, an __operation__ (@op@) and an __intent__.  [Payload]     Internal state of a replica.-[Operation]+[Intent]     User's request to update.-[Update]+[Operation (Op)]     Operation to be applied to other replicas. -For many types /operation/ and /update/ may be the same.+For many types /operation/ and /intent/ may be the same. But for 'CRDT.Cm.LWW.LWW', for instance, this rule doesn't hold: user can request only value, and type attaches a timestamp to it. @@ -50,44 +48,28 @@ Concurrent updates are observed equally.  @-∀ up1 up2 s .-'concurrent' up1 up2 ==>-    'observe' ('updateDownstream' up1 . 'updateDownstream' up2 $ s) ==-    'observe' ('updateDownstream' up2 . 'updateDownstream' up1 $ s)+∀ op1 op2 .+'concurrent' op1 op2 ==> 'apply' op1 . 'apply' op2 == 'apply' op2 . 'apply' op1 @  Idempotency doesn't need to hold. -} -class (CausalOrd u, Eq (View u)) => CmRDT u where-    type Op u-    type Op u = u -- default case--    type Payload u--    type View u-    type View u = Payload u -- default case+class (CausalOrd op, Eq (Payload op)) => CmRDT op where+    type Intent op+    type Intent op = op -- common case -    -- | Precondition for 'updateAtSource'.-    -- Calculates if the operation is applicable to the current state.-    updateAtSourcePre :: Op u -> Payload u -> Bool-    updateAtSourcePre _ _ = True+    type Payload op      -- | Generate an update to the local and remote replicas.-    -- Doesn't have sense if 'updateAtSourcePre' is false.     ---    -- May or may not use clock.-    updateAtSource :: Clock m => Op u -> m u+    -- Returns 'Nothing' if the intended operation is not applicable.+    makeOp :: Intent op -> Payload op -> Maybe (Process op) -    default updateAtSource :: (Clock m, Op u ~ u) => Op u -> m u-    updateAtSource = pure+    default makeOp+        :: Intent op ~ op => Intent op -> Payload op -> Maybe (Process op)+    makeOp i _ = Just $ pure i      -- | Apply an update to the payload.     -- An invalid update must be ignored.-    updateDownstream :: u -> Payload u -> Payload u--    -- | Extract user-visible value from payload-    view :: Payload u -> View u--    default view :: (Payload u ~ View u) => Payload u -> View u-    view = id+    apply :: op -> Payload op -> Payload op
lib/CRDT/Cm/Counter.hs view
@@ -13,12 +13,12 @@     deriving (Bounded, Enum, Eq, Show)  instance (Num a, Eq a) => CmRDT (Counter a) where-    type Payload  (Counter a) = a+    type Payload (Counter a) = a -    updateDownstream = \case+    apply = \case         Increment -> (+ 1)         Decrement -> subtract 1  -- | Empty order, allowing arbitrary reordering instance CausalOrd (Counter a) where-    before _ _ = False+    affects _ _ = False
lib/CRDT/Cm/GSet.hs view
@@ -1,13 +1,9 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-}  module CRDT.Cm.GSet     ( GSet (..)     ) where -import           Prelude hiding (lookup)- import           Data.Set (Set) import qualified Data.Set as Set @@ -17,9 +13,9 @@     deriving (Eq, Show)  instance Ord a => CmRDT (GSet a) where-    type Payload  (GSet a) = Set a+    type Payload (GSet a) = Set a -    updateDownstream (Add a) = Set.insert a+    apply (Add a) = Set.insert a  instance Eq a => CausalOrd (GSet a) where-    before _ _ = False+    affects _ _ = False
− lib/CRDT/Cm/TPSet.hs
@@ -1,34 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE TypeFamilies #-}---- | TODO(cblp, 2017-09-29) USet?-module CRDT.Cm.TPSet-    ( TPSet (..)-    , updateAtSource-    , updateDownstream-    ) where--import           Data.Set (Set)-import qualified Data.Set as Set--import           CRDT.Cm (CausalOrd (..), CmRDT (..))--data TPSet a = Add a | Remove a-    deriving (Eq, Show)--instance Ord a => CmRDT (TPSet a) where-    type Payload  (TPSet a) = Set a--    updateAtSourcePre op payload = case op of-        Add _     -> True-        Remove a  -> Set.member a payload--    updateDownstream = \case-        Add a     -> Set.insert a-        Remove a  -> Set.delete a--instance Eq a => CausalOrd (TPSet a) where-    Remove a `before` Add b = a == b -- `Remove e` can occur only after `Add e`-    _        `before` _     = False  -- Any other are not ordered
+ lib/CRDT/Cm/TwoPSet.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TypeFamilies #-}++-- | TODO(cblp, 2017-09-29) USet?+module CRDT.Cm.TwoPSet+    ( TwoPSet (..)+    ) where++import           Control.Monad (guard)+import           Data.Set (Set)+import qualified Data.Set as Set++import           CRDT.Cm (CausalOrd (..), CmRDT (..))++data TwoPSet a = Add a | Remove a+    deriving (Eq, Show)++instance Ord a => CmRDT (TwoPSet a) where+    type Payload (TwoPSet a) = Set a++    makeOp op s = case op of+        Add _     -> Just (pure op)+        Remove a  -> guard (Set.member a s) *> Just (pure op)++    apply = \case+        Add a     -> Set.insert a+        Remove a  -> Set.delete a++instance Eq a => CausalOrd (TwoPSet a) where+    Remove a `affects` Add b = a == b -- `Remove e` can occur only after `Add e`+    _        `affects` _     = False  -- Any other are not ordered
lib/CRDT/Cv/GCounter.hs view
@@ -37,6 +37,4 @@  -- | Get value from the state query :: Num a => GCounter a -> a-query (GCounter v) = mapSum v-  where-    mapSum = IntMap.foldr (+) 0+query (GCounter v) = sum v
lib/CRDT/Cv/GSet.hs view
@@ -1,21 +1,18 @@-{-# OPTIONS_GHC -Wno-orphans #-}- module CRDT.Cv.GSet     ( GSet     , add     , initial-    , query+    , lookup     ) where -import           Data.Semilattice (Semilattice)+import           Prelude hiding (lookup)+ import           Data.Set (Set) import qualified Data.Set as Set  -- | Grow-only set type GSet = Set -instance Ord a => Semilattice (Set a)- -- | update add :: Ord a => a -> GSet a -> GSet a add = Set.insert@@ -24,5 +21,6 @@ initial :: GSet a initial = Set.empty -query :: Ord a => a -> GSet a -> Bool-query = Set.member+-- | lookup query+lookup :: Ord a => a -> GSet a -> Bool+lookup = Set.member
+ lib/CRDT/Cv/LwwElementSet.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}++module CRDT.Cv.LwwElementSet+    ( LwwElementSet (..)+    , add+    , initial+    , lookup+    , remove+    ) where++import           Prelude hiding (lookup)++import           Data.Foldable (for_)+import           Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import           Data.Semigroup (Semigroup (..))++import           CRDT.LamportClock (Process)+import           CRDT.LWW (LWW, advanceFromLWW)+import qualified CRDT.LWW as LWW+import           Data.Semilattice (Semilattice)++newtype LwwElementSet a = LES (Map a (LWW Bool))+    deriving (Eq, Show)++instance Ord a => Semigroup (LwwElementSet a) where+    LES m1 <> LES m2 = LES (Map.unionWith (<>) m1 m2)++instance Ord a => Semilattice (LwwElementSet a)++initial :: LwwElementSet a+initial = LES Map.empty++add :: Ord a => a -> LwwElementSet a -> Process (LwwElementSet a)+add value old@(LES m) = do+    advanceFromLES old+    tag <- LWW.initial True+    pure . LES $ Map.insertWith (<>) value tag m++remove :: Ord a => a -> LwwElementSet a -> Process (LwwElementSet a)+remove value old@(LES m) = do+    advanceFromLES old+    tag <- LWW.initial False+    pure . LES $ Map.insertWith (<>) value tag m++lookup :: Ord a => a -> LwwElementSet a -> Bool+lookup value (LES m) = maybe False LWW.query $ Map.lookup value m++advanceFromLES :: LwwElementSet a -> Process ()+advanceFromLES (LES m) = for_ m advanceFromLWW
lib/CRDT/Cv/Max.hs view
@@ -1,19 +1,14 @@-{-# OPTIONS_GHC -Wno-orphans #-}- module CRDT.Cv.Max     ( Max (..)-    , point+    , initial     , query     ) where  import           Data.Semigroup (Max (..))-import           Data.Semilattice (Semilattice) -instance Ord a => Semilattice (Max a)- -- | Construct new value-point :: a -> Max a-point = Max+initial :: a -> Max a+initial = Max  query :: Max a -> a query = getMax
+ lib/CRDT/Cv/ORSet.hs view
@@ -0,0 +1,48 @@+module CRDT.Cv.ORSet+    ( ORSet (..)+    , add+    , initial+    , remove+    , lookup+    ) where++import           Prelude hiding (lookup)++import           Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import           Data.Maybe (fromMaybe)+import           Data.Semigroup (Semigroup, (<>))+import           Numeric.Natural (Natural)++import           CRDT.LamportClock (Pid, Process, getPid)+import           Data.Semilattice (Semilattice)++type Tag = (Pid, Natural)++newtype ORSet a = ORSet (Map a (Map Tag Bool))+    deriving (Eq, Show)++unpack :: ORSet a -> Map a (Map Tag Bool)+unpack (ORSet s) = s++instance Ord a => Semigroup (ORSet a) where+    ORSet s1 <> ORSet s2 = ORSet $ Map.unionWith (Map.unionWith (&&)) s1 s2++instance Ord a => Semilattice (ORSet a)++initial :: ORSet a+initial = ORSet Map.empty++add :: Ord a => a -> ORSet a -> Process (ORSet a)+add a (ORSet s) = do+    pid <- getPid+    pure $ ORSet $ Map.alter (add1 pid) a s+  where+    add1 pid = Just . add2 pid . fromMaybe Map.empty+    add2 pid tags = Map.insert (pid, fromIntegral $ length tags) True tags++remove :: Ord a => a -> ORSet a -> ORSet a+remove a (ORSet s) = ORSet $ Map.adjust (Map.map $ const False) a s++lookup :: Ord a => a -> ORSet a -> Bool+lookup e = or . fromMaybe Map.empty . Map.lookup e . unpack
+ lib/CRDT/Cv/TwoPSet.hs view
@@ -0,0 +1,45 @@+module CRDT.Cv.TwoPSet+    ( TwoPSet (..)+    , add+    , initial+    , lookup+    , remove+    , singleton+    , isKnown+    ) where++import           Prelude hiding (lookup)++import           Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import           Data.Maybe (fromMaybe)+import           Data.Semigroup (Semigroup ((<>)))++import           Data.Semilattice (Semilattice)++newtype TwoPSet a = TwoPSet (Map a Bool)+    deriving (Eq, Show)++instance Ord a => Semigroup (TwoPSet a) where+    TwoPSet m1 <> TwoPSet m2 = TwoPSet (Map.unionWith (&&) m1 m2)++instance Ord a => Semilattice (TwoPSet a)++add :: Ord a => a -> TwoPSet a -> TwoPSet a+add e (TwoPSet m) = TwoPSet (Map.insertWith (&&) e True m)++initial :: TwoPSet a+initial = TwoPSet Map.empty++lookup :: Ord a => a -> TwoPSet a -> Bool+lookup e (TwoPSet m) = fromMaybe False $ Map.lookup e m++remove :: Ord a => a -> TwoPSet a -> TwoPSet a+remove e (TwoPSet m) = TwoPSet $ Map.adjust (const False) e m++singleton :: Ord a => a -> TwoPSet a+singleton a = add a initial++-- | XXX Internal TODO remove+isKnown :: Ord a => a -> TwoPSet a -> Bool+isKnown e (TwoPSet m) = Map.member e m
lib/CRDT/LWW.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeFamilies #-} @@ -8,49 +7,54 @@     , initial     , assign     , query-      -- * CmRDT-    , Assign (..)+      -- * Implementation detail+    , advanceFromLWW     ) where -import           Data.Function (on) import           Data.Semigroup (Semigroup, (<>))+ import           Data.Semilattice (Semilattice)  import           CRDT.Cm (CausalOrd (..), CmRDT (..))-import           LamportClock (Clock (newTimestamp), Timestamp)+import           CRDT.LamportClock (LamportTime, Process, advance, getTime)  -- | Last write wins. Assuming timestamp is unique. -- This type is both 'CmRDT' and 'CvRDT'.+--+-- Timestamps are assumed unique, totally ordered,+-- and consistent with causal order;+-- i.e., if assignment 1 happened-before assignment 2,+-- the former’s timestamp is less than the latter’s. data LWW a = LWW-    { value     :: !a-    , timestamp :: !Timestamp+    { value :: !a+    , time  :: !LamportTime     }-    deriving (Show)+    deriving (Eq, Show)  -------------------------------------------------------------------------------- -- CvRDT ----------------------------------------------------------------------- -instance Eq (LWW a) where-    (==) = (==) `on` timestamp--instance Ord (LWW a) where-    (<=) = (<=) `on` timestamp- -- | Merge by choosing more recent timestamp.-instance Semigroup (LWW a) where-    (<>) = max+instance Eq a => Semigroup (LWW a) where+    x@(LWW xv xt) <> y@(LWW yv yt)+        | xt < yt = y+        | yt < xt = x+        | xv == yv = x+        | otherwise = error "LWW assumes timestamps to be unique"  -- | See 'CvRDT'-instance Semilattice (LWW a)+instance Eq a => Semilattice (LWW a)  -- | Initialize state-initial :: Clock f => a -> f (LWW a)-initial value = LWW value <$> newTimestamp+initial :: a -> Process (LWW a)+initial value = LWW value <$> getTime  -- | Change state as CvRDT operation. -- Current value is ignored, because new timestamp is always greater.-assign :: Clock f => a -> LWW a -> f (LWW a)-assign value _ = LWW value <$> newTimestamp+assign :: a -> LWW a -> Process (LWW a)+assign value old = do+    advanceFromLWW old+    initial value  -- | Query state query :: LWW a -> a@@ -60,19 +64,15 @@ -- CmRDT -----------------------------------------------------------------------  instance CausalOrd (LWW a) where-    before _ _ = False---- | Change state as CmRDT operation-newtype Assign a = Assign a-    deriving (Eq, Show)+    affects _ _ = False  instance Eq a => CmRDT (LWW a) where-    type Op       (LWW a) = Assign a-    type Payload  (LWW a) = LWW a-    type View     (LWW a) = a+    type Intent  (LWW a) = a+    type Payload (LWW a) = LWW a -    updateAtSource (Assign value) = LWW value <$> newTimestamp+    makeOp value = Just . assign value -    updateDownstream = (<>)+    apply = (<>) -    view = value+advanceFromLWW :: LWW a -> Process ()+advanceFromLWW LWW{time} = advance time
+ lib/CRDT/LamportClock.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module CRDT.LamportClock+    ( Pid (..)+    -- * Lamport timestamp (for a single process)+    , LamportTime (..)+    , getTime+    , advance+    -- * Lamport clock (for a whole multi-process system)+    , LamportClock (..)+    , runLamportClock+    -- * Process+    , Process (..)+    , getPid+    , runProcess+    ) where++import           Control.Monad.Reader (ReaderT, ask, runReaderT)+import           Control.Monad.State.Strict (State, evalState, modify, state)+import           Control.Monad.Trans (lift)+import           Data.IntMap.Strict (IntMap)+import qualified Data.IntMap.Strict as IntMap+import           Data.Maybe (fromMaybe)+import           Numeric.Natural (Natural)++type LocalTime = Natural++data LamportTime = LamportTime !LocalTime !Pid+    deriving (Eq, Ord, Show)++-- | Unique process identifier+newtype Pid = Pid Int+    deriving (Eq, Ord, Show)++-- | Key is 'Pid'. Non-present value is equivalent to 0.+newtype LamportClock a = LamportClock (State (IntMap LocalTime) a)+    deriving (Applicative, Functor, Monad)++newtype Process a = Process (ReaderT Pid LamportClock a)+    deriving (Applicative, Functor, Monad)++getPid :: Process Pid+getPid = Process ask++getTime :: Process LamportTime+getTime = Process $ do+    pid <- ask+    time <- lift $ preIncrementAt pid+    pure $ LamportTime time pid++runLamportClock :: LamportClock a -> a+runLamportClock (LamportClock action) = evalState action mempty++runProcess :: Pid -> Process a -> LamportClock a+runProcess pid (Process action) = runReaderT action pid++preIncrementAt :: Pid -> LamportClock LocalTime+preIncrementAt (Pid pid) =+    LamportClock . state $ \m -> let+        lt' = succ . fromMaybe 0 $ IntMap.lookup pid m+        in (lt', IntMap.insert pid lt' m)++advance :: LamportTime -> Process ()+advance (LamportTime time _) = Process $ do+    Pid pid <- ask+    lift . LamportClock . modify $ IntMap.insertWith max pid time
lib/Data/Semilattice.hs view
@@ -3,7 +3,8 @@     , merge     ) where -import           Data.Semigroup (Semigroup, (<>))+import           Data.Semigroup (Max, Semigroup, (<>))+import           Data.Set (Set)  {- | A semilattice.@@ -29,3 +30,9 @@ merge = (<>) infixr 6 `merge` {-# INLINE merge #-}++-- instances for external types++instance Ord a => Semilattice (Max a)++instance Ord a => Semilattice (Set a)
− lib/LamportClock.hs
@@ -1,89 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}--module LamportClock-    ( Pid (..)-    , Time-    , Timestamp (..)-    , Clock (..)-    , LamportClock-    , runLamportClock-    , Process-    , runProcess-    , barrier-    ) where--import           Control.Arrow (first)-import           Control.Monad.Reader (ReaderT, ask, runReaderT)-import           Control.Monad.State.Strict (MonadState, State, evalState,-                                             modify, state)-import           Data.Functor (($>))-import           Data.IntMap.Strict (IntMap)-import qualified Data.IntMap.Strict as IntMap-import           Data.Maybe (fromMaybe)--type Time = Word---- | Unique process identifier-newtype Pid = Pid Int-    deriving (Eq, Ord, Show)--unPid :: Pid -> Int-unPid (Pid pid) = pid---- | Key is 'Pid'. Non-present value is equivalent to 0.--- TODO(cblp, 2017-09-28) Use bounded-intmap-type LamportTime = IntMap Time--type LamportClock = State LamportTime---- | XXX Make sure all subsequent calls to 'newTimestamp' return timestamps--- greater than all prior calls.-barrier :: [Pid] -> LamportClock ()-barrier pids =-    modify $ \clocks -> let-        selectedClocks = lamportTimeFromList-            [(pid, fromMaybe 0 $ lamportTimeLookup pid clocks) | pid <- pids]-        in-        if null selectedClocks then-            clocks-        else-            IntMap.union-                (selectedClocks $> succ (maximum selectedClocks))-                clocks---- | Timestamps are assumed unique, totally ordered,--- and consistent with causal order;--- i.e., if assignment 1 happened-before assignment 2,--- the former’s timestamp is less than the latter’s.-data Timestamp = Timestamp !Time !Pid-    deriving (Eq, Ord, Show)--class Applicative f => Clock f where-    -- | Get another unique timestamp-    newTimestamp :: f Timestamp--type Process = ReaderT Pid LamportClock--instance Clock Process where-    newTimestamp = do-        pid <- ask-        time <- postIncrementAt pid-        pure $ Timestamp time pid--runLamportClock :: LamportClock a -> a-runLamportClock action = evalState action mempty--runProcess :: Pid -> Process a -> LamportClock a-runProcess pid action = runReaderT action pid--postIncrementAt :: MonadState LamportTime m => Pid -> m Time-postIncrementAt (Pid pid) = state $ \m ->-    let v = fromMaybe 0 $ IntMap.lookup pid m-    in  (v, IntMap.insert pid (v + 1) m)--lamportTimeFromList :: [(Pid, Time)] -> LamportTime-lamportTimeFromList = IntMap.fromList . map (first unPid)--lamportTimeLookup :: Pid -> LamportTime -> Maybe Time-lamportTimeLookup = IntMap.lookup . unPid
test/ArbitraryOrphans.hs view
@@ -5,40 +5,47 @@  module ArbitraryOrphans () where -import           Test.Tasty.QuickCheck (Arbitrary (..), arbitraryBoundedEnum,-                                        oneof)+import           Test.QuickCheck (Arbitrary (..), arbitraryBoundedEnum,+                                  elements)  import           CRDT.Cm.Counter (Counter (..)) import           CRDT.Cm.GSet (GSet (..))-import           CRDT.Cm.TPSet (TPSet (..))-import qualified CRDT.Cm.TPSet as TPSet+import           CRDT.Cm.TwoPSet (TwoPSet (..))+import qualified CRDT.Cm.TwoPSet as TwoPSet+import           CRDT.Cv.ORSet (ORSet (..)) import           CRDT.Cv.GCounter (GCounter (..))+import           CRDT.Cv.LwwElementSet (LwwElementSet (..)) import           CRDT.Cv.Max (Max (..)) import           CRDT.Cv.PNCounter (PNCounter (..))-import           CRDT.LWW (Assign (..), LWW (..))-import           LamportClock (Pid (..), Timestamp (..))+import qualified CRDT.Cv.TwoPSet as Cv+import           CRDT.LamportClock (LamportTime (..), Pid (..))+import           CRDT.LWW (LWW (..))  instance Arbitrary (Counter a) where     arbitrary = arbitraryBoundedEnum -deriving instance Arbitrary a => Arbitrary (Assign a)- instance Arbitrary a => Arbitrary (LWW a) where     arbitrary = LWW <$> arbitrary <*> arbitrary  deriving instance Arbitrary a => Arbitrary (GSet a) -instance Arbitrary a => Arbitrary (TPSet a) where-    arbitrary = oneof [TPSet.Add <$> arbitrary, Remove <$> arbitrary]+instance Arbitrary a => Arbitrary (TwoPSet a) where+    arbitrary = elements [TwoPSet.Add, Remove] <*> arbitrary  deriving instance Arbitrary a => Arbitrary (GCounter a) +deriving instance (Arbitrary a, Ord a) => Arbitrary (ORSet a)++deriving instance (Arbitrary a, Ord a) => Arbitrary (LwwElementSet a)+ deriving instance Arbitrary a => Arbitrary (Max a)  instance Arbitrary a => Arbitrary (PNCounter a) where     arbitrary = PNCounter <$> arbitrary <*> arbitrary -deriving instance Arbitrary Pid+deriving instance (Ord a, Arbitrary a) => Arbitrary (Cv.TwoPSet a) -instance Arbitrary Timestamp where-    arbitrary = Timestamp <$> arbitrary <*> arbitrary+instance Arbitrary LamportTime where+    arbitrary = LamportTime <$> arbitrary <*> arbitrary++deriving instance Arbitrary Pid
+ test/Cm/TwoPSet.hs view
@@ -0,0 +1,13 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}++{-# LANGUAGE TypeApplications #-}++module Cm.TwoPSet where++import           CRDT.Cm.TwoPSet (TwoPSet)++import           Laws (cmrdtLaw)++prop_Cm = cmrdtLaw @(TwoPSet Char)++-- prop_add =
test/Counter.hs view
@@ -1,13 +1,11 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+ {-# LANGUAGE TypeApplications #-} -module Counter-    ( counter-    ) where+module Counter where  import           CRDT.Cm.Counter (Counter)-import           Test.Tasty (TestTree, testGroup)  import           Laws (cmrdtLaw) -counter :: TestTree-counter = testGroup "Counter" [cmrdtLaw @(Counter Int)]+prop_Cm = cmrdtLaw @(Counter Int)
+ test/Cv/TwoPSet.hs view
@@ -0,0 +1,27 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}++{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Cv.TwoPSet where++import           Prelude hiding (lookup)++import           Test.QuickCheck ((==>))++import           CRDT.Cv.TwoPSet (TwoPSet (..), add, isKnown, lookup, remove)++import           Laws (cvrdtLaws)++-- | Difference from LwwElementSet and ORSet -- removal bias+prop_removal_bias (s :: TwoPSet Char) x =+    not . lookup x . add x . remove x $ add x s++prop_add (s :: TwoPSet Char) x = not (isKnown x s) ==> lookup x (add x s)++prop_remove (s :: TwoPSet Char) x = not . lookup x $ remove x s++prop_add_then_remove (s :: TwoPSet Char) x =+    not . lookup x . remove x $ add x s++test_Cv = cvrdtLaws @(TwoPSet Char) Nothing
test/GCounter.hs view
@@ -1,21 +1,17 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module GCounter-    ( gCounter-    ) where+module GCounter where -import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (testProperty)+import           Test.QuickCheck ((===))  import           CRDT.Cv.GCounter (GCounter (..), increment, query)  import           Laws (cvrdtLaws) -gCounter :: TestTree-gCounter = testGroup "GCounter"-    [ cvrdtLaws @(GCounter Int)-    , testProperty "increment" $-        \(counter :: GCounter Int) i ->-            query (increment i counter) == succ (query counter)-    ]+test_Cv = cvrdtLaws @(GCounter Int) Nothing++prop_increment (counter :: GCounter Int) pid =+    query (increment pid counter) === succ (query counter)
test/GSet.hs view
@@ -1,22 +1,17 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module GSet-    ( gSet-    ) where--import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (testProperty, (==>))+module GSet where  import qualified CRDT.Cm.GSet as Cm import qualified CRDT.Cv.GSet as Cv  import           Laws (cmrdtLaw, cvrdtLaws) -gSet :: TestTree-gSet = testGroup "GSet"-    [ cmrdtLaw @(Cm.GSet Int)-    , cvrdtLaws @(Cv.GSet Int)-    , testProperty "Cv.add" $ \(set :: Cv.GSet Int) i ->-        not (Cv.query i set) ==> Cv.query i (Cv.add i set)-    ]+prop_Cm = cmrdtLaw @(Cm.GSet Char)++test_Cv = cvrdtLaws @(Cv.GSet Char) Nothing++prop_add (x :: Char) = Cv.lookup x . Cv.add x
test/LWW.hs view
@@ -1,37 +1,44 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}+{-# OPTIONS_GHC -Wno-missing-signatures #-}++{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module LWW (lww) where+module LWW+    ( genUniquelyTimedLWW+    , prop_Cm+    , prop_assign+    , prop_merge_with_former+    , test_Cv+    ) where +import           Control.Monad.State.Strict (StateT, lift) import           Data.Semigroup ((<>))-import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (testProperty)+import           Data.Set (Set)+import qualified Data.Set as Set+import           Test.QuickCheck (Arbitrary, Gen, arbitrary, (===)) -import           CRDT.LWW (LWW (..), assign, initial, query)-import           LamportClock (barrier, runLamportClock, runProcess)+import           CRDT.LamportClock (LamportTime, runLamportClock, runProcess)+import           CRDT.LWW (LWW (LWW), assign, initial, query)  import           Laws (cmrdtLaw, cvrdtLaws)+import           QCUtil (genUnique) -lww :: TestTree-lww = testGroup "LWW"-    [ cmrdtLaw @(LWW Int)-    , testGroup "Cv"-        [ cvrdtLaws @(LWW Int)-        , testProperty "assign" $-            \pid1 pid2 (formerValue :: Int) latterValue ->-                runLamportClock $ do-                    state1 <- runProcess pid1 $ initial formerValue-                    barrier [pid1, pid2]-                    state2 <- runProcess pid2 $ assign latterValue state1-                    pure $ query state2 == latterValue-        , testProperty "merge with former" $-            \pid1 pid2 (formerValue :: Int) latterValue ->-                runLamportClock $ do-                    state1 <- runProcess pid1 $ initial formerValue-                    barrier [pid1, pid2]-                    state2 <- runProcess pid2 $ initial latterValue-                    pure $ query (state1 <> state2) == latterValue-        ]-    ]+prop_Cm = cmrdtLaw @(LWW Char)++test_Cv = cvrdtLaws @(LWW Char) $ Just (genUniquelyTimedLWW, Set.empty)++prop_assign pid1 pid2 (formerValue :: Char) latterValue = runLamportClock $ do+    state1 <- runProcess pid1 $ initial formerValue+    state2 <- runProcess pid2 $ assign latterValue state1+    pure $ query state2 === latterValue++prop_merge_with_former pid1 pid2 (formerValue :: Char) latterValue =+    runLamportClock $ do+        state1 <- runProcess pid1 $ initial formerValue+        state2 <- runProcess pid2 $ assign latterValue state1+        pure $ query (state1 <> state2) === latterValue++-- | Generate specified number of 'LWW' with unique timestamps+genUniquelyTimedLWW :: Arbitrary a => StateT (Set LamportTime) Gen (LWW a)+genUniquelyTimedLWW = LWW <$> lift arbitrary <*> genUnique
test/Laws.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -9,57 +9,80 @@     , cvrdtLaws     ) where -import           Control.Monad (unless)-import           Data.Function ((&))+import           Control.Monad.State.Strict (StateT, evalStateT) import           Data.Semigroup (Semigroup, (<>))-import           Data.Semilattice (Semilattice, merge)-import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (Arbitrary (..), discard, testProperty,-                                        (===), (==>))+import           Test.QuickCheck (Arbitrary (..), Gen, Property, discard,+                                  forAll, property, (===), (==>))+import           Test.Tasty (TestTree)+import           Test.Tasty.QuickCheck (testProperty)  import           CRDT.Cm (CmRDT (..), concurrent) import           CRDT.Cv (CvRDT)-import           LamportClock (Clock, runLamportClock, runProcess)+import           CRDT.LamportClock (runLamportClock, runProcess)+import           Data.Semilattice (Semilattice, merge)  import           ArbitraryOrphans () -semigroupLaw :: forall a . (Arbitrary a, Semigroup a, Eq a, Show a) => TestTree-semigroupLaw =-    testGroup "Semigroup law" [testProperty "associativity" associativity]+gen2 :: (StateT s Gen a, s) -> Gen (a, a)+gen2 (gen, start) = evalStateT ((,) <$> gen <*> gen) start++gen3 :: (StateT s Gen a, s) -> Gen (a, a, a)+gen3 (gen, start) = evalStateT ((,,) <$> gen <*> gen <*> gen) start++semigroupLaw+    :: forall a+    . (Arbitrary a, Semigroup a, Eq a, Show a)+    => Maybe (Gen (a, a, a)) -> TestTree+semigroupLaw mgen = testProperty "associativity" $ associativity' mgen   where-    associativity (x, y, z :: a) = (x <> y) <> z == x <> (y <> z)+    associativity x y (z :: a) = (x <> y) <> z === x <> (y <> z)+    associativity' = \case+        Nothing  -> property associativity+        Just gen -> forAll gen $ uncurry3 associativity  semilatticeLaws-    :: forall a . (Arbitrary a, Semilattice a, Eq a, Show a) => TestTree-semilatticeLaws = testGroup "Semilattice laws"-    [ semigroupLaw @a-    , testProperty "commutativity" commutativity+    :: forall a s+    . (Arbitrary a, Semilattice a, Eq a, Show a)+    => Maybe (StateT s Gen a, s) -> [TestTree]+semilatticeLaws mgen =+    [ semigroupLaw $ gen3 <$> mgen+    , testProperty "commutativity" $ commutativity' $ gen2 <$> mgen     , testProperty "idempotency"   idempotency     ]   where-    commutativity (x, y :: a) = x `merge` y === y `merge` x     idempotency (x :: a) = x `merge` x === x+    commutativity x (y :: a) = x `merge` y === y `merge` x+    commutativity' = \case+        Nothing  -> property commutativity+        Just gen -> forAll gen $ uncurry commutativity -cvrdtLaws :: forall a . (Arbitrary a, CvRDT a, Eq a, Show a) => TestTree-cvrdtLaws = semilatticeLaws @a+cvrdtLaws+    :: forall a s+    . (Arbitrary a, CvRDT a, Eq a, Show a)+    => Maybe (StateT s Gen a, s) -> [TestTree]+cvrdtLaws = semilatticeLaws +-- | CmRDT law: concurrent ops commute cmrdtLaw-    :: forall u-    . ( CmRDT u-      , Arbitrary (Op u), Show (Op u)-      , Arbitrary (Payload u), Show (Payload u)-      , Show (View u)-      )-    => TestTree-cmrdtLaw = testProperty "CmRDT law: concurrent ops commute" $-    \pid (state0 :: Payload u) (op1, op2 :: Op u) ->-        runLamportClock $ runProcess pid $ do-            up1 <- updateAtSourceIfCan op1 state0-            up2 <- updateAtSourceIfCan op2 state0-            let state12 = state0 & updateDownstream up1 & updateDownstream up2-            let state21 = state0 & updateDownstream up2 & updateDownstream up1-            pure $ concurrent up1 up2 ==> view @u state12 === view @u state21+    :: forall op.+    ( CmRDT op+    , Arbitrary op, Show op+    , Arbitrary (Intent op), Show (Intent op)+    , Arbitrary (Payload op), Show (Payload op)+    )+    => Property+cmrdtLaw = property $ \(s :: Payload op) in1 in2 pid1 pid2 ->+    whenJust (makeOp @op in1 s) $ \getOp1 ->+    whenJust (makeOp @op in2 s) $ \getOp2 -> let+        (op1, op2) =+            runLamportClock $+            (,) <$> runProcess pid1 getOp1 <*> runProcess pid2 getOp2+        in+        concurrent op1 op2 ==>+            (apply op1 . apply op2) s === (apply op2 . apply op1) s   where-    updateAtSourceIfCan :: Clock m => Op u -> Payload u -> m u-    updateAtSourceIfCan op state =-        unless (updateAtSourcePre @u op state) discard *> updateAtSource op+    whenJust Nothing  _ = discard+    whenJust (Just a) f = f a++uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c
+ test/LwwElementSet.hs view
@@ -0,0 +1,65 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}++{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module LwwElementSet+    ( prop_add+    , prop_no_removal_bias+    , prop_remove+    , prop_they_accidentally_delete_our_value+    , test_Cv+    ) where++import           Prelude hiding (lookup)++import           Control.Monad (replicateM)+import           Control.Monad.State.Strict (StateT, lift)+import qualified Data.Map.Strict as Map+import           Data.Semigroup ((<>))+import           Data.Set (Set)+import qualified Data.Set as Set+import           Test.QuickCheck (Arbitrary, Gen, arbitrary)++import           CRDT.Cv.LwwElementSet (LwwElementSet (..), add, lookup, remove)+import           CRDT.LamportClock (LamportTime, runLamportClock, runProcess)++import           Laws (cvrdtLaws)+import           LWW (genUniquelyTimedLWW)++test_Cv =+    cvrdtLaws @(LwwElementSet Char) $ Just (genUniquelyTimedLES, Set.empty)++-- | Generate specified number of 'LWW' with unique timestamps+genUniquelyTimedLES+    :: (Arbitrary a, Ord a) => StateT (Set LamportTime) Gen (LwwElementSet a)+genUniquelyTimedLES = do+    values <- lift arbitrary+    tags <- replicateM (length values) genUniquelyTimedLWW+    pure $ LES $ Map.fromList $ zip values tags++prop_add (s :: LwwElementSet Char) x pid1 =+    runLamportClock $ do+        s1 <- runProcess pid1 $ add x s+        pure $ lookup x s1++prop_remove (s :: LwwElementSet Char) x pid1 pid2 =+    runLamportClock $ do+        s1 <- runProcess pid1 $ add x s+        s2 <- runProcess pid2 $ remove x s1+        pure . not $ lookup x s2++-- | Difference from 'TwoPSet' -- no removal bias+prop_no_removal_bias (s :: LwwElementSet Char) x pid1 pid2 pid3 =+    runLamportClock $ do+        s1 <- runProcess pid1 $ add x s+        s2 <- runProcess pid2 $ remove x s1+        s3 <- runProcess pid3 $ add x s2+        pure $ lookup x s3++-- | Difference from 'ORSet' -- other replica can accidentally delete x+prop_they_accidentally_delete_our_value (s :: LwwElementSet Char) x pid1 pid2 =+    runLamportClock $ do+        s1 <- runProcess pid1 $ add x s+        s2 <- runProcess pid2 $ remove x =<< add x s+        pure . not . lookup x $ s1 <> s2
test/Main.hs view
@@ -1,13 +1,1 @@-import           Test.Tasty (defaultMain, testGroup)--import           Counter (counter)-import           GCounter (gCounter)-import           GSet (gSet)-import           LWW (lww)-import           Max (maxTest)-import           PNCounter (pnCounter)-import           TPSet (tpSet)--main :: IO ()-main = defaultMain $-    testGroup "" [counter, gCounter, gSet, lww, maxTest, pnCounter, tpSet]+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}
test/Max.hs view
@@ -1,23 +1,17 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module Max-    ( maxTest-    ) where+module Max where -import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (testProperty)+import           Test.QuickCheck ((===)) -import           CRDT.Cv.Max (Max, point, query)+import           CRDT.Cv.Max (Max, initial, query) import           Data.Semilattice (merge)  import           Laws (cvrdtLaws) -maxTest :: TestTree-maxTest = testGroup "Max"-    [ testGroup "Cv"-        [ cvrdtLaws @(Max Int)-        , testProperty "merge" $-            \(m :: Max Int) i -> query (point i `merge` m) == max i (query m)-        ]-    ]+test_Cv = cvrdtLaws @(Max Char) Nothing++prop_merge (x :: Char) y = query (initial x `merge` initial y) === max x y
+ test/ORSet.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}++{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module ORSet where++import           Prelude hiding (lookup)++import           Data.Semigroup (Semigroup ((<>)))++import           CRDT.Cv.ORSet (ORSet (..), add, lookup, remove)+import           CRDT.LamportClock (runLamportClock, runProcess)++import           Laws (cvrdtLaws)++test_Cv = cvrdtLaws @(ORSet Int) Nothing++prop_add pid (x :: Char) s =+    runLamportClock $ runProcess pid $ not . lookup x . remove x <$> add x s++-- | Difference from 'LwwElementSet' --+-- other replica can not accidentally delete x+prop_add_merge (x :: Char) pid s1 s0 =+    runLamportClock $ runProcess pid $ lookup x . (<> s1) <$> add x s0
test/PNCounter.hs view
@@ -1,25 +1,21 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module PNCounter-    ( pnCounter-    ) where+module PNCounter where -import           Test.Tasty (TestTree, testGroup)-import           Test.Tasty.QuickCheck (testProperty)+import           Test.QuickCheck ((===))  import           CRDT.Cv.PNCounter (PNCounter (..), decrement, increment, query)  import           GCounter () import           Laws (cvrdtLaws) -pnCounter :: TestTree-pnCounter = testGroup "PNCounter"-    [ cvrdtLaws @(PNCounter Int)-    , testProperty "increment" $-        \(counter :: PNCounter Int) i ->-            query (increment i counter) == succ (query counter)-    , testProperty "decrement" $-        \(counter :: PNCounter Int) i ->-            query (decrement i counter) == pred (query counter)-    ]+test_Cv = cvrdtLaws @(PNCounter Int) Nothing++prop_increment (counter :: PNCounter Int) pid =+    query (increment pid counter) === succ (query counter)++prop_decrement (counter :: PNCounter Int) pid =+    query (decrement pid counter) === pred (query counter)
+ test/QCUtil.hs view
@@ -0,0 +1,15 @@+module QCUtil+    ( genUnique+    ) where++import           Control.Monad.State.Strict (StateT, get, lift, modify)+import           Data.Set (Set)+import qualified Data.Set as Set+import           Test.QuickCheck (Arbitrary, Gen, arbitrary, suchThat)++genUnique :: (Arbitrary a, Ord a) => StateT (Set a) Gen a+genUnique = do+    used <- get+    a <- lift $ arbitrary `suchThat` (`Set.notMember` used)+    modify $ Set.insert a+    pure a
− test/TPSet.hs
@@ -1,13 +0,0 @@-{-# LANGUAGE TypeApplications #-}--module TPSet-    ( tpSet-    ) where--import           CRDT.Cm.TPSet (TPSet)-import           Test.Tasty (TestTree, testGroup)--import           Laws (cmrdtLaw)--tpSet :: TestTree-tpSet = testGroup "TPSet" [cmrdtLaw @(TPSet Int)]