crdt 0.5 → 1.0
raw patch · 36 files changed
+753/−396 lines, 36 filesdep +enummapsetdep +latticesdep +microlensPVP ok
version bump matches the API change (PVP)
Dependencies added: enummapset, lattices, microlens, microlens-ghc, microlens-mtl, mtl
API changes (from Hackage documentation)
- CRDT.Cm: type family State op :: Type;
- CRDT.Cm: update :: CmRDT op => op -> State op -> State op
- CRDT.Cm: }
- CRDT.GCounter.Cm: Increment :: GCounter a
- CRDT.GCounter.Cm: data GCounter a
- CRDT.GCounter.Cm: initial :: Num a => a
- CRDT.GCounter.Cm: instance GHC.Num.Num a => CRDT.Cm.CmRDT (CRDT.GCounter.Cm.GCounter a)
- CRDT.GCounter.Cm: instance GHC.Show.Show (CRDT.GCounter.Cm.GCounter a)
- CRDT.GCounter.Cv: data GCounter a
- CRDT.GCounter.Cv: increment :: Num a => Word -> GCounter a -> GCounter a
- CRDT.GCounter.Cv: initial :: GCounter a
- CRDT.GCounter.Cv: query :: Num a => GCounter a -> a
- CRDT.GCounter.Cv.Internal: GCounter :: (IntMap a) -> GCounter a
- CRDT.GCounter.Cv.Internal: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.GCounter.Cv.Internal.GCounter a)
- CRDT.GCounter.Cv.Internal: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.GCounter.Cv.Internal.GCounter a)
- CRDT.GCounter.Cv.Internal: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.GCounter.Cv.Internal.GCounter a)
- CRDT.GCounter.Cv.Internal: instance GHC.Show.Show a => GHC.Show.Show (CRDT.GCounter.Cv.Internal.GCounter a)
- CRDT.GCounter.Cv.Internal: newtype GCounter a
- CRDT.GSet.Cv: add :: Ord a => a -> GSet a -> GSet a
- CRDT.GSet.Cv: initial :: GSet a
- CRDT.GSet.Cv: query :: Ord a => a -> GSet a -> Bool
- CRDT.GSet.Cv: type GSet = Set
- CRDT.GSet.Cv.Internal: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Set.Base.Set a)
- CRDT.GSet.Cv.Internal: type GSet = Set
- CRDT.LWW: Write :: !Timestamp -> !a -> LWW a
- CRDT.LWW: [timestamp] :: LWW a -> !Timestamp
- CRDT.LWW: [value] :: LWW a -> !a
- CRDT.LWW: data LWW a
- CRDT.LWW: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Ord a => CRDT.Cm.CmRDT (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Classes.Ord a => GHC.Classes.Ord (CRDT.LWW.LWW a)
- CRDT.LWW: instance GHC.Show.Show a => GHC.Show.Show (CRDT.LWW.LWW a)
- CRDT.LWW: point :: Timestamp -> a -> LWW a
- CRDT.LWW: query :: LWW a -> a
- CRDT.LWW: write :: Ord a => Timestamp -> a -> LWW a -> LWW a
- CRDT.PNCounter.Cm: Decrement :: PNCounter a
- CRDT.PNCounter.Cm: Increment :: PNCounter a
- CRDT.PNCounter.Cm: data PNCounter a
- CRDT.PNCounter.Cm: initial :: Num a => a
- CRDT.PNCounter.Cm: instance GHC.Enum.Bounded (CRDT.PNCounter.Cm.PNCounter a)
- CRDT.PNCounter.Cm: instance GHC.Enum.Enum (CRDT.PNCounter.Cm.PNCounter a)
- CRDT.PNCounter.Cm: instance GHC.Num.Num a => CRDT.Cm.CmRDT (CRDT.PNCounter.Cm.PNCounter a)
- CRDT.PNCounter.Cm: instance GHC.Show.Show (CRDT.PNCounter.Cm.PNCounter a)
- CRDT.PNCounter.Cv: data PNCounter a
- CRDT.PNCounter.Cv: decrement :: Num a => Word -> PNCounter a -> PNCounter a
- CRDT.PNCounter.Cv: increment :: Num a => Word -> PNCounter a -> PNCounter a
- CRDT.PNCounter.Cv: initial :: PNCounter a
- CRDT.PNCounter.Cv: query :: Num a => PNCounter a -> a
- CRDT.PNCounter.Cv.Internal: PNCounter :: !(GCounter a) -> !(GCounter a) -> PNCounter a
- CRDT.PNCounter.Cv.Internal: [negative] :: PNCounter a -> !(GCounter a)
- CRDT.PNCounter.Cv.Internal: [positive] :: PNCounter a -> !(GCounter a)
- CRDT.PNCounter.Cv.Internal: data PNCounter a
- CRDT.PNCounter.Cv.Internal: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.PNCounter.Cv.Internal.PNCounter a)
- CRDT.PNCounter.Cv.Internal: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.PNCounter.Cv.Internal.PNCounter a)
- CRDT.PNCounter.Cv.Internal: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.PNCounter.Cv.Internal.PNCounter a)
- CRDT.PNCounter.Cv.Internal: instance GHC.Show.Show a => GHC.Show.Show (CRDT.PNCounter.Cv.Internal.PNCounter a)
- CRDT.Timestamp: type Timestamp = Natural
- Data.Semilattice: slappend :: Semilattice a => a -> a -> a
+ CRDT.Cm: concurrent :: PartialOrd a => a -> a -> Bool
+ CRDT.Cm: updateAtSource :: (CmRDT payload op update, Clock m) => op -> m update
+ CRDT.Cm: updateAtSourcePre :: CmRDT payload op update => op -> payload -> Bool
+ CRDT.Cm: updateDownstream :: CmRDT payload op update => update -> payload -> payload
+ CRDT.Cm.Counter: Counter :: a -> Counter a
+ CRDT.Cm.Counter: Decrement :: CounterOp a
+ CRDT.Cm.Counter: Increment :: CounterOp a
+ CRDT.Cm.Counter: data CounterOp a
+ CRDT.Cm.Counter: initial :: Num a => Counter a
+ CRDT.Cm.Counter: instance (GHC.Num.Num a, GHC.Classes.Eq a) => CRDT.Cm.CmRDT (CRDT.Cm.Counter.Counter a) (CRDT.Cm.Counter.CounterOp a) (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance Algebra.PartialOrd.PartialOrd (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance Data.Observe.Observe (CRDT.Cm.Counter.Counter a)
+ CRDT.Cm.Counter: instance GHC.Classes.Eq (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance GHC.Enum.Bounded (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance GHC.Enum.Enum (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance GHC.Show.Show (CRDT.Cm.Counter.CounterOp a)
+ CRDT.Cm.Counter: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.Counter.Counter a)
+ CRDT.Cm.Counter: newtype Counter a
+ CRDT.Cm.GSet: Add :: a -> Add a
+ CRDT.Cm.GSet: initial :: GSet a
+ CRDT.Cm.GSet: instance Data.Observe.Observe (CRDT.Cv.GSet.GSet a)
+ CRDT.Cm.GSet: instance GHC.Classes.Eq a => Algebra.PartialOrd.PartialOrd (CRDT.Cm.GSet.Add a)
+ CRDT.Cm.GSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cm.GSet.Add a)
+ CRDT.Cm.GSet: instance GHC.Classes.Ord a => CRDT.Cm.CmRDT (Data.Set.Base.Set a) (CRDT.Cm.GSet.Add a) (CRDT.Cm.GSet.Add a)
+ CRDT.Cm.GSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.GSet.Add a)
+ CRDT.Cm.GSet: lookup :: Ord a => a -> GSet a -> Bool
+ CRDT.Cm.GSet: newtype Add a
+ CRDT.Cm.GSet: type GSet = Set
+ CRDT.Cm.LWW: Assign :: a -> Assign a
+ CRDT.Cm.LWW: LWW :: !Timestamp -> !a -> LWW a
+ CRDT.Cm.LWW: [timestamp] :: LWW a -> !Timestamp
+ CRDT.Cm.LWW: [value] :: LWW a -> !a
+ CRDT.Cm.LWW: data LWW a
+ CRDT.Cm.LWW: instance Algebra.PartialOrd.PartialOrd (CRDT.Cv.LWW.LWW a)
+ CRDT.Cm.LWW: instance Data.Observe.Observe (CRDT.Cv.LWW.LWW a)
+ CRDT.Cm.LWW: instance GHC.Classes.Eq a => CRDT.Cm.CmRDT (CRDT.Cv.LWW.LWW a) (CRDT.Cm.LWW.Assign a) (CRDT.Cv.LWW.LWW a)
+ CRDT.Cm.LWW: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cm.LWW.Assign a)
+ CRDT.Cm.LWW: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.LWW.Assign a)
+ CRDT.Cm.LWW: newtype Assign a
+ CRDT.Cm.TPSet: Add :: a -> TPSetOp a
+ CRDT.Cm.TPSet: Remove :: a -> TPSetOp a
+ CRDT.Cm.TPSet: TPSet :: Set a -> TPSet a
+ CRDT.Cm.TPSet: [payload] :: TPSet a -> Set a
+ CRDT.Cm.TPSet: data TPSetOp a
+ CRDT.Cm.TPSet: initial :: TPSet a
+ CRDT.Cm.TPSet: instance Data.Observe.Observe (CRDT.Cm.TPSet.TPSet a)
+ CRDT.Cm.TPSet: instance GHC.Classes.Eq a => Algebra.PartialOrd.PartialOrd (CRDT.Cm.TPSet.TPSetOp a)
+ CRDT.Cm.TPSet: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cm.TPSet.TPSetOp a)
+ CRDT.Cm.TPSet: instance GHC.Classes.Ord a => CRDT.Cm.CmRDT (CRDT.Cm.TPSet.TPSet a) (CRDT.Cm.TPSet.TPSetOp a) (CRDT.Cm.TPSet.TPSetOp a)
+ CRDT.Cm.TPSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.TPSet.TPSet a)
+ CRDT.Cm.TPSet: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cm.TPSet.TPSetOp a)
+ CRDT.Cm.TPSet: lookup :: Ord a => a -> TPSet a -> Bool
+ CRDT.Cm.TPSet: newtype TPSet a
+ CRDT.Cm.TPSet: updateAtSource :: (CmRDT payload op update, Clock m) => op -> m update
+ CRDT.Cm.TPSet: updateDownstream :: CmRDT payload op update => update -> payload -> payload
+ CRDT.Cv.GCounter: GCounter :: (IntMap a) -> GCounter a
+ CRDT.Cv.GCounter: increment :: Num a => Word -> GCounter a -> GCounter a
+ CRDT.Cv.GCounter: initial :: GCounter a
+ CRDT.Cv.GCounter: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cv.GCounter.GCounter a)
+ CRDT.Cv.GCounter: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.Cv.GCounter.GCounter a)
+ CRDT.Cv.GCounter: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.Cv.GCounter.GCounter a)
+ CRDT.Cv.GCounter: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.GCounter.GCounter a)
+ CRDT.Cv.GCounter: newtype GCounter a
+ CRDT.Cv.GCounter: query :: Num a => GCounter a -> a
+ CRDT.Cv.GSet: add :: Ord a => a -> GSet a -> GSet a
+ CRDT.Cv.GSet: initial :: GSet a
+ 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.GSet: type GSet = Set
+ CRDT.Cv.LWW: LWW :: !Timestamp -> !a -> LWW a
+ CRDT.Cv.LWW: [timestamp] :: LWW a -> !Timestamp
+ CRDT.Cv.LWW: [value] :: LWW a -> !a
+ CRDT.Cv.LWW: assign :: Clock f => a -> LWW a -> f (LWW a)
+ CRDT.Cv.LWW: data LWW a
+ CRDT.Cv.LWW: initial :: Clock f => a -> f (LWW a)
+ CRDT.Cv.LWW: instance Data.Semigroup.Semigroup (CRDT.Cv.LWW.LWW a)
+ CRDT.Cv.LWW: instance Data.Semilattice.Semilattice (CRDT.Cv.LWW.LWW a)
+ CRDT.Cv.LWW: instance GHC.Classes.Eq (CRDT.Cv.LWW.LWW a)
+ CRDT.Cv.LWW: instance GHC.Classes.Ord (CRDT.Cv.LWW.LWW a)
+ CRDT.Cv.LWW: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.LWW.LWW a)
+ CRDT.Cv.LWW: query :: LWW a -> a
+ CRDT.Cv.Max: Max :: a -> Max a
+ CRDT.Cv.Max: [getMax] :: Max a -> a
+ CRDT.Cv.Max: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (Data.Semigroup.Max a)
+ CRDT.Cv.Max: newtype Max a :: * -> *
+ CRDT.Cv.Max: point :: a -> Max a
+ CRDT.Cv.Max: query :: Max a -> a
+ CRDT.Cv.PNCounter: PNCounter :: !(GCounter a) -> !(GCounter a) -> PNCounter a
+ CRDT.Cv.PNCounter: [negative] :: PNCounter a -> !(GCounter a)
+ CRDT.Cv.PNCounter: [positive] :: PNCounter a -> !(GCounter a)
+ CRDT.Cv.PNCounter: data PNCounter a
+ CRDT.Cv.PNCounter: decrement :: Num a => Word -> PNCounter a -> PNCounter a
+ CRDT.Cv.PNCounter: increment :: Num a => Word -> PNCounter a -> PNCounter a
+ CRDT.Cv.PNCounter: initial :: PNCounter a
+ CRDT.Cv.PNCounter: instance GHC.Classes.Eq a => GHC.Classes.Eq (CRDT.Cv.PNCounter.PNCounter a)
+ CRDT.Cv.PNCounter: instance GHC.Classes.Ord a => Data.Semigroup.Semigroup (CRDT.Cv.PNCounter.PNCounter a)
+ CRDT.Cv.PNCounter: instance GHC.Classes.Ord a => Data.Semilattice.Semilattice (CRDT.Cv.PNCounter.PNCounter a)
+ CRDT.Cv.PNCounter: instance GHC.Show.Show a => GHC.Show.Show (CRDT.Cv.PNCounter.PNCounter a)
+ CRDT.Cv.PNCounter: query :: Num a => PNCounter a -> a
+ Data.Observe: class Observe a where type Observed a :: Type where {
+ Data.Observe: observe :: Observe a => a -> Observed a
+ Data.Observe: type family Observed a :: Type;
+ Data.Observe: }
+ Data.Semilattice: merge :: Semilattice a => a -> a -> a
+ LamportClock: Pid :: Word32 -> 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.Enum.Enum LamportClock.Pid
+ 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 (EnumMap Pid Time)
+ LamportClock: type Process = ReaderT Pid LamportClock
+ LamportClock: type Time = Word
- CRDT.Cm: class CmRDT op where type State op :: Type where {
+ CRDT.Cm: class (Observe payload, Eq (Observed payload), PartialOrd update) => CmRDT payload op update | payload -> op, op -> update, update -> payload where updateAtSourcePre _ _ = True
- Data.Semilattice: infixr 6 `slappend`
+ Data.Semilattice: infixr 6 `merge`
Files
- crdt.cabal +24/−11
- lib/CRDT/Cm.hs +60/−8
- lib/CRDT/Cm/Counter.hs +41/−0
- lib/CRDT/Cm/GSet.hs +43/−0
- lib/CRDT/Cm/LWW.hs +35/−0
- lib/CRDT/Cm/TPSet.hs +54/−0
- lib/CRDT/Cv.hs +2/−1
- lib/CRDT/Cv/GCounter.hs +42/−0
- lib/CRDT/Cv/GSet.hs +28/−0
- lib/CRDT/Cv/LWW.hs +48/−0
- lib/CRDT/Cv/Max.hs +19/−0
- lib/CRDT/Cv/PNCounter.hs +59/−0
- lib/CRDT/GCounter/Cm.hs +0/−22
- lib/CRDT/GCounter/Cv.hs +0/−31
- lib/CRDT/GCounter/Cv/Internal.hs +0/−16
- lib/CRDT/GSet/Cv.hs +0/−21
- lib/CRDT/GSet/Cv/Internal.hs +0/−12
- lib/CRDT/LWW.hs +0/−44
- lib/CRDT/PNCounter/Cm.hs +0/−23
- lib/CRDT/PNCounter/Cv.hs +0/−41
- lib/CRDT/PNCounter/Cv/Internal.hs +0/−22
- lib/CRDT/Timestamp.hs +0/−7
- lib/Data/Observe.hs +11/−0
- lib/Data/Semilattice.hs +5/−5
- lib/LamportClock.hs +75/−0
- lib/Lens/Micro/Extra.hs +29/−0
- test/ArbitraryOrphans.hs +47/−0
- test/Counter.hs +13/−0
- test/GCounter.hs +6/−27
- test/GSet.hs +7/−10
- test/LWW.hs +22/−41
- test/Laws.hs +32/−22
- test/Main.hs +6/−2
- test/Max.hs +23/−0
- test/PNCounter.hs +9/−30
- test/TPSet.hs +13/−0
crdt.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: crdt-version: 0.5+version: 1.0 synopsis: Conflict-free replicated data types description: Definitions of CmRDT and CvRDT. Implementations for some classic CRDTs. category: Distributed Systems@@ -26,20 +26,29 @@ build-depends: base >= 4.9 && < 4.10 , containers+ , enummapset+ , lattices+ , microlens+ , microlens-ghc+ , microlens-mtl+ , mtl exposed-modules: CRDT.Cm+ CRDT.Cm.Counter+ CRDT.Cm.GSet+ CRDT.Cm.LWW+ CRDT.Cm.TPSet CRDT.Cv- CRDT.GCounter.Cm- CRDT.GCounter.Cv- CRDT.GCounter.Cv.Internal- CRDT.GSet.Cv- CRDT.GSet.Cv.Internal- CRDT.LWW- CRDT.PNCounter.Cm- CRDT.PNCounter.Cv- CRDT.PNCounter.Cv.Internal- CRDT.Timestamp+ CRDT.Cv.GCounter+ CRDT.Cv.GSet+ CRDT.Cv.LWW+ CRDT.Cv.Max+ CRDT.Cv.PNCounter+ Data.Observe Data.Semilattice+ LamportClock+ other-modules:+ Lens.Micro.Extra default-language: Haskell2010 test-suite test@@ -55,9 +64,13 @@ , tasty-quickcheck , crdt other-modules:+ ArbitraryOrphans+ Counter GCounter GSet Laws LWW+ Max PNCounter+ TPSet default-language: Haskell2010
lib/CRDT/Cm.hs view
@@ -1,24 +1,76 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeFamilies #-} module CRDT.Cm ( CmRDT (..)+ , concurrent ) where -import Data.Kind (Type)+import Algebra.PartialOrd (PartialOrd (leq))+import Data.Observe (Observe (..)) +import LamportClock (Clock)++-- | TODO(cblp, 2017-09-29) import from lattices >= 1.6+comparable :: PartialOrd a => a -> a -> Bool+comparable a b = a `leq` b || b `leq` a++-- | Not comparable, i. e. ¬(a ≤ b) ∧ ¬(b ≤ a).+concurrent :: PartialOrd a => a -> a -> Bool+concurrent a b = not $ comparable a b+ {- | Operation-based, or commutative (Cm) replicated data type. -[Commutativity law]+== Implementation - @'update' op1 . 'update' op2 == 'update' op2 . 'update' op1@+In Haskell, a CmRDT implementation consists of 3 types —+a __payload__, an __operation__ (@op@) and an __update__. +[Payload]+ Internal state of a replica.+[Operation]+ User's request to update.+[Update]+ Operation to be applied to other replicas.++For many types /operation/ and /update/ 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.++== Additional constraint — commutativity law++Concurrent updates are observed equally.++@+∀ up1 up2 s .+'concurrent' up1 up2 ==>+ 'observe' ('updateDownstream' up1 . 'updateDownstream' up2 $ s) ==+ 'observe' ('updateDownstream' up2 . 'updateDownstream' up1 $ s)+@+ Idempotency doesn't need to hold. -}-class CmRDT op where - -- | The type of the target value- type State op :: Type+class (Observe payload, Eq (Observed payload), PartialOrd update)+ => CmRDT payload op update+ | payload -> op, op -> update, update -> payload+ where - -- | Apply operation to a value- update :: op -> State op -> State op+ -- | Precondition for 'updateAtSource'.+ -- Calculates if the operation is applicable to the current state.+ updateAtSourcePre :: op -> payload -> Bool+ updateAtSourcePre _ _ = True++ -- | 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 -> m update++ -- | Apply an update to the payload.+ -- An invalid update must be ignored.+ updateDownstream :: update -> payload -> payload
+ lib/CRDT/Cm/Counter.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}++module CRDT.Cm.Counter+ ( Counter (..)+ , CounterOp (..)+ , initial+ ) where++import Algebra.PartialOrd (PartialOrd (..))+import Data.Observe (Observe (..))++import CRDT.Cm (CmRDT (..))++newtype Counter a = Counter a+ deriving (Show)++data CounterOp a = Increment | Decrement+ deriving (Bounded, Enum, Eq, Show)++instance (Num a, Eq a) => CmRDT (Counter a) (CounterOp a) (CounterOp a) where+ updateAtSource = pure+ updateDownstream = opToFunc++instance Observe (Counter a) where+ type Observed (Counter a) = a+ observe (Counter c) = c++-- | Empty order, allowing arbitrary reordering+instance PartialOrd (CounterOp a) where+ leq _ _ = False++initial :: Num a => Counter a+initial = Counter 0++opToFunc :: Num a => CounterOp a -> Counter a -> Counter a+opToFunc op (Counter c) =+ Counter $ case op of+ Increment -> c + 1+ Decrement -> c - 1
+ lib/CRDT/Cm/GSet.hs view
@@ -0,0 +1,43 @@+{-# OPTIONS_GHC -Wno-orphans #-}++{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}++module CRDT.Cm.GSet+ ( GSet+ , Add (..)+ , initial+ , lookup+ ) where++import Prelude hiding (lookup)++import Algebra.PartialOrd (PartialOrd (leq))+import Data.Observe (Observe (..))+import Data.Set (Set)+import qualified Data.Set as Set++import CRDT.Cm (CmRDT (..))+import CRDT.Cv.GSet (GSet)++initial :: GSet a+initial = Set.empty++-- | query lookup+lookup :: Ord a => a -> GSet a -> Bool+lookup = Set.member++newtype Add a = Add a+ deriving (Eq, Show)++instance Ord a => CmRDT (Set a) (Add a) (Add a) where+ updateAtSource = pure+ updateDownstream (Add a) = Set.insert a++instance Observe (GSet a) where+ type Observed (GSet a) = Set a+ observe = id++instance Eq a => PartialOrd (Add a) where+ leq _ _ = False
+ lib/CRDT/Cm/LWW.hs view
@@ -0,0 +1,35 @@+{-# OPTIONS_GHC -Wno-orphans #-} -- TODO(cblp, 2017-10-02) join with Cv.LWW++{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeFamilies #-}++module CRDT.Cm.LWW+ ( LWW (..)+ , Assign (..)+ ) where++import Algebra.PartialOrd (PartialOrd (..))+import Data.Observe (Observe (..))+import Data.Semigroup ((<>))+import Lens.Micro ((<&>))++import CRDT.Cm (CmRDT (..))+import CRDT.Cv.LWW (LWW (..))+import LamportClock (Clock (newTimestamp))++instance PartialOrd (LWW a) where+ leq _ _ = False++newtype Assign a = Assign a+ deriving (Eq, Show)++instance Eq a => CmRDT (LWW a) (Assign a) (LWW a) where+ updateAtSource (Assign value) =+ newTimestamp <&> \t -> LWW{timestamp = t, value}+ updateDownstream = (<>)++instance Observe (LWW a) where+ type Observed (LWW a) = a+ observe = value
+ lib/CRDT/Cm/TPSet.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeFamilies #-}++-- | TODO(cblp, 2017-09-29) USet?+module CRDT.Cm.TPSet+ ( TPSet (..)+ , TPSetOp (..)+ , initial+ , lookup+ , updateAtSource+ , updateDownstream+ ) where++import Prelude hiding (lookup)++import Algebra.PartialOrd (PartialOrd (..))+import Data.Observe (Observe (..))+import Data.Set (Set)+import qualified Data.Set as Set++import CRDT.Cm (CmRDT (..))++newtype TPSet a = TPSet{payload :: Set a}+ deriving (Show)++data TPSetOp a = Add a | Remove a+ deriving (Eq, Show)++initial :: TPSet a+initial = TPSet Set.empty++-- | query lookup+lookup :: Ord a => a -> TPSet a -> Bool+lookup a TPSet{payload} = Set.member a payload++instance Ord a => CmRDT (TPSet a) (TPSetOp a) (TPSetOp a) where+ updateAtSourcePre op payload = case op of+ Add _ -> True+ Remove a -> lookup a payload++ updateAtSource = pure++ updateDownstream op TPSet{payload} = case op of+ Add a -> TPSet{payload = Set.insert a payload}+ Remove a -> TPSet{payload = Set.delete a payload}++instance Observe (TPSet a) where+ type Observed (TPSet a) = Set a+ observe = payload++instance Eq a => PartialOrd (TPSetOp a) where+ leq (Remove a) (Add b) = a == b -- `Remove e` can occur only after `Add e`+ leq _ _ = False -- Any other are not ordered
lib/CRDT/Cv.hs view
@@ -4,7 +4,7 @@ ( CvRDT ) where -import Data.Semilattice (Semilattice)+import Data.Semilattice (Semilattice) {- | State-based, or convergent (Cv) replicated data type.@@ -13,6 +13,7 @@ Query function is not needed. State itself is exposed. In other words, @query = 'id'@.+Some types may offer more convenient query functions. Actually, a CvRDT is nothing more a 'Semilattice'. -}
+ lib/CRDT/Cv/GCounter.hs view
@@ -0,0 +1,42 @@+module CRDT.Cv.GCounter+ ( GCounter (..)+ , initial+ , query+ -- * Operation+ , increment+ ) where++import Data.IntMap.Strict (IntMap)+import qualified Data.IntMap.Strict as IntMap+import Data.Semigroup (Semigroup ((<>)))++import CRDT.Cv (CvRDT)++-- | Grow-only counter.+newtype GCounter a = GCounter (IntMap a)+ deriving (Eq, Show)++instance Ord a => Semigroup (GCounter a) where+ GCounter x <> GCounter y = GCounter $ IntMap.unionWith max x y++instance Ord a => CvRDT (GCounter a)++-- | Increment counter+increment+ :: Num a+ => Word -- ^ replica id+ -> GCounter a+ -> GCounter a+increment replicaId (GCounter imap) = GCounter (IntMap.insertWith (+) i 1 imap)+ where+ i = fromIntegral replicaId++-- | Initial state+initial :: GCounter a+initial = GCounter IntMap.empty++-- | Get value from the state+query :: Num a => GCounter a -> a+query (GCounter v) = mapSum v+ where+ mapSum = IntMap.foldr (+) 0
+ lib/CRDT/Cv/GSet.hs view
@@ -0,0 +1,28 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module CRDT.Cv.GSet+ ( GSet+ , add+ , initial+ , query+ ) where++import Data.Semilattice (Semilattice)+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++-- | initialization+initial :: GSet a+initial = Set.empty++query :: Ord a => a -> GSet a -> Bool+query = Set.member
+ lib/CRDT/Cv/LWW.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeFamilies #-}++module CRDT.Cv.LWW+ ( LWW (..)+ , initial+ , assign+ , query+ ) where++import Data.Function (on)+import Data.Semigroup (Semigroup, (<>))+import Lens.Micro ((<&>))++import CRDT.Cv (CvRDT)+import LamportClock (Clock (newTimestamp), Timestamp)++-- | Last write wins. Assuming timestamp is unique.+data LWW a = LWW+ { timestamp :: !Timestamp+ , value :: !a+ }+ deriving (Show)++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 CvRDT (LWW a)++-- | Initialize state+initial :: Clock f => a -> f (LWW a)+initial value = newTimestamp <&> \timestamp -> LWW{timestamp = timestamp, value}++-- | Change state+assign :: Clock f => a -> LWW a -> f (LWW a)+assign value cur = newTimestamp <&> \timestamp -> cur <> LWW{timestamp, value}++-- | Query state+query :: LWW a -> a+query = value
+ lib/CRDT/Cv/Max.hs view
@@ -0,0 +1,19 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module CRDT.Cv.Max+ ( Max (..)+ , point+ , 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++query :: Max a -> a+query = getMax
+ lib/CRDT/Cv/PNCounter.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE NamedFieldPuns #-}++module CRDT.Cv.PNCounter+ ( PNCounter (..)+ , initial+ , query+ -- * Operations+ , decrement+ , increment+ ) where++import Data.Semigroup (Semigroup (..))++import CRDT.Cv (CvRDT)+import CRDT.Cv.GCounter (GCounter)+import qualified CRDT.Cv.GCounter as GCounter++{- |+Positive-negative counter. Allows incrementing and decrementing.+Nice example of combining of existing CvRDT ('GCounter' in this case)+to create another CvRDT.+-}+data PNCounter a = PNCounter+ { positive :: !(GCounter a)+ , negative :: !(GCounter a)+ }+ deriving (Eq, Show)++instance Ord a => Semigroup (PNCounter a) where+ PNCounter p1 n1 <> PNCounter p2 n2 = PNCounter (p1 <> p2) (n1 <> n2)++instance Ord a => CvRDT (PNCounter a)++-- | Get value from the state+query :: Num a => PNCounter a -> a+query PNCounter{positive, negative} =+ GCounter.query positive - GCounter.query negative++-- | Decrement counter+decrement+ :: Num a+ => Word -- ^ replica id+ -> PNCounter a+ -> PNCounter a+decrement i pnc@PNCounter{negative} =+ pnc{negative = GCounter.increment i negative}++-- | Increment counter+increment+ :: Num a+ => Word -- ^ replica id+ -> PNCounter a+ -> PNCounter a+increment i pnc@PNCounter{positive} =+ pnc{positive = GCounter.increment i positive}++-- | Initial state+initial :: PNCounter a+initial = PNCounter{positive = GCounter.initial, negative = GCounter.initial}
− lib/CRDT/GCounter/Cm.hs
@@ -1,22 +0,0 @@-{-# LANGUAGE TypeFamilies #-}--module CRDT.GCounter.Cm- ( GCounter (..)- , initial- ) where--import CRDT.Cm (CmRDT, State, update)---- | Grow-only counter.------ Commutativity: 'Increment' obviously commutes with itself.-data GCounter a = Increment- deriving (Show)--instance Num a => CmRDT (GCounter a) where- type State (GCounter a) = a- update _ = (+1)---- | Initial state-initial :: Num a => a-initial = 0
− lib/CRDT/GCounter/Cv.hs
@@ -1,31 +0,0 @@-module CRDT.GCounter.Cv- ( GCounter- , initial- , query- -- * Operation- , increment- ) where--import qualified Data.IntMap.Strict as IntMap--import CRDT.GCounter.Cv.Internal---- | Increment counter-increment- :: Num a- => Word -- ^ replica id- -> GCounter a- -> GCounter a-increment replicaId (GCounter imap) = GCounter (IntMap.insertWith (+) i 1 imap)- where- i = fromIntegral replicaId---- | Initial state-initial :: GCounter a-initial = GCounter IntMap.empty---- | Get value from the state-query :: Num a => GCounter a -> a-query (GCounter v) = mapSum v- where- mapSum = IntMap.foldr (+) 0
− lib/CRDT/GCounter/Cv/Internal.hs
@@ -1,16 +0,0 @@-module CRDT.GCounter.Cv.Internal where--import Data.Semigroup (Semigroup ((<>)))-import Data.IntMap.Strict (IntMap)-import qualified Data.IntMap.Strict as IntMap--import CRDT.Cv (CvRDT)---- | Grow-only counter.-newtype GCounter a = GCounter (IntMap a)- deriving (Eq, Show)--instance Ord a => Semigroup (GCounter a) where- GCounter x <> GCounter y = GCounter $ IntMap.unionWith max x y--instance Ord a => CvRDT (GCounter a)
− lib/CRDT/GSet/Cv.hs
@@ -1,21 +0,0 @@-module CRDT.GSet.Cv- ( GSet- , add- , initial- , query- ) where--import qualified Data.Set as Set--import CRDT.GSet.Cv.Internal---- | update-add :: Ord a => a -> GSet a -> GSet a-add = Set.insert---- | initialization-initial :: GSet a-initial = Set.empty--query :: Ord a => a -> GSet a -> Bool-query = Set.member
− lib/CRDT/GSet/Cv/Internal.hs
@@ -1,12 +0,0 @@-{-# OPTIONS_GHC -Wno-orphans #-}--module CRDT.GSet.Cv.Internal where--import Data.Set (Set)--import CRDT.Cv (CvRDT)---- | Grow-only set-type GSet = Set--instance Ord a => CvRDT (Set a)
− lib/CRDT/LWW.hs
@@ -1,44 +0,0 @@-{-# LANGUAGE TypeFamilies #-}--module CRDT.LWW- ( LWW (..)- , point- , write- , query- ) where--import Data.Semigroup (Semigroup (..))--import CRDT.Cm (CmRDT, State)-import qualified CRDT.Cm as Cm-import CRDT.Cv (CvRDT)-import CRDT.Timestamp (Timestamp)---- | Last write wins. Interesting, this type is both 'CmRDT' and 'CvRDT'.-data LWW a = Write- { timestamp :: !Timestamp- , value :: !a- }- deriving (Eq, Ord, Show)---- | Merge by choosing more recent timestamp.-instance Ord a => Semigroup (LWW a) where- (<>) = max--instance Ord a => CmRDT (LWW a) where- type State (LWW a) = LWW a- update = max--instance Ord a => CvRDT (LWW a)---- | Initialize state-point :: Timestamp -> a -> LWW a-point = Write---- | Change state-write :: Ord a => Timestamp -> a -> LWW a -> LWW a-write t v s = Write t v <> s---- | Query state-query :: LWW a -> a-query = value
− lib/CRDT/PNCounter/Cm.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE TypeFamilies #-}--module CRDT.PNCounter.Cm- ( PNCounter (..)- , initial- ) where--import CRDT.Cm (CmRDT, State, update)---- | Positive-negative counter. Allows incrementing and decrementing.-data PNCounter a = Increment | Decrement- deriving (Bounded, Enum, Show)--instance Num a => CmRDT (PNCounter a) where- type State (PNCounter a) = a- update = \case- Increment -> (+1)- Decrement -> subtract 1---- | Initial state-initial :: Num a => a-initial = 0
− lib/CRDT/PNCounter/Cv.hs
@@ -1,41 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}--module CRDT.PNCounter.Cv- ( PNCounter- , initial- , query- -- * Operations- , decrement- , increment- ) where--import qualified CRDT.GCounter.Cv as GCounter--import CRDT.PNCounter.Cv.Internal---- | Get value from the state-query :: Num a => PNCounter a -> a-query PNCounter{positive, negative} =- GCounter.query positive - GCounter.query negative---- | Decrement counter-decrement- :: Num a- => Word -- ^ replica id- -> PNCounter a- -> PNCounter a-decrement i pnc@PNCounter{negative} =- pnc{negative = GCounter.increment i negative}---- | Increment counter-increment- :: Num a- => Word -- ^ replica id- -> PNCounter a- -> PNCounter a-increment i pnc@PNCounter{positive} =- pnc{positive = GCounter.increment i positive}---- | Initial state-initial :: PNCounter a-initial = PNCounter{positive = GCounter.initial, negative = GCounter.initial}
− lib/CRDT/PNCounter/Cv/Internal.hs
@@ -1,22 +0,0 @@-module CRDT.PNCounter.Cv.Internal where--import Data.Semigroup (Semigroup (..))--import CRDT.Cv (CvRDT)-import CRDT.GCounter.Cv (GCounter)--{- |-Positive-negative counter. Allows incrementing and decrementing.-Nice example of combining of existing CvRDT ('GCounter' in this case)-to create another CvRDT.--}-data PNCounter a = PNCounter- { positive :: !(GCounter a)- , negative :: !(GCounter a)- }- deriving (Eq, Show)--instance Ord a => Semigroup (PNCounter a) where- PNCounter p1 n1 <> PNCounter p2 n2 = PNCounter (p1 <> p2) (n1 <> n2)--instance Ord a => CvRDT (PNCounter a)
− lib/CRDT/Timestamp.hs
@@ -1,7 +0,0 @@-module CRDT.Timestamp- ( Timestamp- ) where--import Numeric.Natural (Natural)--type Timestamp = Natural
+ lib/Data/Observe.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE TypeFamilies #-}++module Data.Observe+ ( Observe (..)+ ) where++import Data.Kind (Type)++class Observe a where+ type Observed a :: Type+ observe :: a -> Observed a
lib/Data/Semilattice.hs view
@@ -1,6 +1,6 @@ module Data.Semilattice ( Semilattice- , slappend+ , merge ) where import Data.Semigroup (Semigroup, (<>))@@ -25,7 +25,7 @@ class Semigroup a => Semilattice a -- | Just ('Semigroup.<>'), specialized to 'Semilattice'.-slappend :: Semilattice a => a -> a -> a-slappend = (<>)-infixr 6 `slappend`-{-# INLINE slappend #-}+merge :: Semilattice a => a -> a -> a+merge = (<>)+infixr 6 `merge`+{-# INLINE merge #-}
+ lib/LamportClock.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeSynonymInstances #-}++module LamportClock+ ( Pid (..)+ , Time+ , Timestamp (..)+ , Clock (..)+ , LamportClock+ , runLamportClock+ , Process+ , runProcess+ , barrier+ ) where++import Control.Monad.Reader (ReaderT, ask, runReaderT)+import Control.Monad.State.Strict (State, evalState, modify)+import Data.EnumMap.Strict (EnumMap)+import qualified Data.EnumMap.Strict as EnumMap+import Data.Functor (($>))+import Data.Maybe (fromMaybe)+import Data.Word (Word32)+import Lens.Micro (at, non)+import Lens.Micro.Extra ((<<+=))++type Time = Word++-- | Unique process identifier+newtype Pid = Pid Word32+ deriving (Enum, Eq, Ord, Show)++-- | TODO(cblp, 2017-09-28) Use bounded-intmap+type LamportClock = State (EnumMap Pid Time)++-- | XXX Make sure all subsequent calls to 'newTimestamp' return timestamps+-- greater than all prior calls.+barrier :: [Pid] -> LamportClock ()+barrier pids =+ modify $ \clocks -> let+ selectedClocks = EnumMap.fromList+ [(pid, fromMaybe 0 $ EnumMap.lookup pid clocks) | pid <- pids]+ in+ if null selectedClocks then+ clocks+ else+ EnumMap.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 <- at pid . non 0 <<+= 1+ 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
+ lib/Lens/Micro/Extra.hs view
@@ -0,0 +1,29 @@+{-# OPTIONS_GHC -Wno-orphans #-}++{-# LANGUAGE TypeFamilies #-}++module Lens.Micro.Extra ((<<+=)) where++import Control.Monad.State (MonadState)+import Data.EnumMap.Strict (EnumMap, enumMapToIntMap, intMapToEnumMap)+import Lens.Micro (LensLike', at, ix)+import Lens.Micro.GHC ()+import Lens.Micro.Internal (At, Index, IxValue, Ixed)+import Lens.Micro.Mtl ((<<%=))++(<<+=) :: (Num a, MonadState s m) => LensLike' ((,) a) s a -> a -> m a+alens <<+= n = alens <<%= (+ n)+infix 4 <<+=+{-# INLINE (<<+=) #-}++type instance Index (EnumMap k v) = k++type instance IxValue (EnumMap k v) = v++instance Enum k => Ixed (EnumMap k v) where+ ix k f m = intMapToEnumMap <$> ix (fromEnum k) f (enumMapToIntMap m)+ {-# INLINE ix #-}++instance Enum k => At (EnumMap k v) where+ at k f m = intMapToEnumMap <$> at (fromEnum k) f (enumMapToIntMap m)+ {-# INLINE at #-}
+ test/ArbitraryOrphans.hs view
@@ -0,0 +1,47 @@+{-# OPTIONS_GHC -Wno-orphans #-}++{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-}++module ArbitraryOrphans () where++import Test.QuickCheck (Arbitrary (..), arbitraryBoundedEnum, oneof)++import CRDT.Cm.Counter (Counter (..), CounterOp (..))+import CRDT.Cm.GSet (Add (..))+import CRDT.Cm.LWW (Assign (..), LWW (..))+import CRDT.Cm.TPSet (TPSet (..), TPSetOp (..))+import qualified CRDT.Cm.TPSet as TPSet+import CRDT.Cv.GCounter (GCounter (..))+import CRDT.Cv.Max (Max (..))+import CRDT.Cv.PNCounter (PNCounter (..))+import LamportClock (Pid (..), Timestamp (..))++deriving instance Arbitrary a => Arbitrary (Counter a)++instance Arbitrary (CounterOp a) where+ arbitrary = arbitraryBoundedEnum++deriving instance Arbitrary a => Arbitrary (Add a)++deriving instance Arbitrary a => Arbitrary (Assign a)++instance Arbitrary a => Arbitrary (LWW a) where+ arbitrary = LWW <$> arbitrary <*> arbitrary++deriving instance (Arbitrary a, Ord a) => Arbitrary (TPSet a)++instance Arbitrary a => Arbitrary (TPSetOp a) where+ arbitrary = oneof [TPSet.Add <$> arbitrary, Remove <$> arbitrary]++deriving instance Arbitrary a => Arbitrary (GCounter a)++deriving instance Arbitrary a => Arbitrary (Max a)++instance Arbitrary a => Arbitrary (PNCounter a) where+ arbitrary = PNCounter <$> arbitrary <*> arbitrary++deriving instance Arbitrary Pid++instance Arbitrary Timestamp where+ arbitrary = Timestamp <$> arbitrary <*> arbitrary
+ test/Counter.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE TypeApplications #-}++module Counter+ ( counter+ ) where++import CRDT.Cm.Counter (Counter)+import Test.Tasty (TestTree, testGroup)++import Laws (cmrdtLaw)++counter :: TestTree+counter = testGroup "Counter" [cmrdtLaw @(Counter Int)]
test/GCounter.hs view
@@ -1,42 +1,21 @@-{-# OPTIONS_GHC -Wno-orphans #-}--{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} module GCounter ( gCounter ) where -import Test.QuickCheck (Arbitrary, arbitrary) import Test.Tasty (TestTree, testGroup) import Test.Tasty.QuickCheck (testProperty) -import CRDT.Cm (update)-import qualified CRDT.GCounter.Cm as Cm-import qualified CRDT.GCounter.Cv as Cv-import qualified CRDT.GCounter.Cv.Internal as Cv--import Laws (cmrdtLaws, cvrdtLaws)--instance Arbitrary (Cm.GCounter a) where- arbitrary = pure Cm.Increment+import CRDT.Cv.GCounter (GCounter (..), increment, query) -deriving instance Arbitrary a => Arbitrary (Cv.GCounter a)+import Laws (cvrdtLaws) gCounter :: TestTree gCounter = testGroup "GCounter"- [ testGroup "Cv"- [ cvrdtLaws @(Cv.GCounter Int)- , testProperty "increment" $- \(counter :: Cv.GCounter Int) i ->- Cv.query (Cv.increment i counter)- == succ (Cv.query counter)- ]- , testGroup "Cm"- [ cmrdtLaws @(Cm.GCounter Int)- , testProperty "increment" $- \(counter :: Int) -> update Cm.Increment counter == succ counter- ]+ [ cvrdtLaws @(GCounter Int)+ , testProperty "increment" $+ \(counter :: GCounter Int) i ->+ query (increment i counter) == succ (query counter) ]
test/GSet.hs view
@@ -1,5 +1,3 @@-{-# OPTIONS_GHC -Wno-orphans #-}- {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -10,16 +8,15 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.QuickCheck (testProperty, (==>)) -import qualified CRDT.GSet.Cv as Cv+import CRDT.Cm.GSet (GSet)+import CRDT.Cv.GSet (add, query) -import Laws (cvrdtLaws)+import Laws (cmrdtLaw, cvrdtLaws) gSet :: TestTree gSet = testGroup "GSet"- [ testGroup "Cv"- [ cvrdtLaws @(Cv.GSet Int)- , testProperty "add" $- \(set :: Cv.GSet Int) i ->- not (Cv.query i set) ==> Cv.query i (Cv.add i set)- ]+ [ cmrdtLaw @(GSet Int)+ , cvrdtLaws @(GSet Int)+ , testProperty "Cv.add" $+ \(set :: GSet Int) i -> not (query i set) ==> query i (add i set) ]
test/LWW.hs view
@@ -1,57 +1,38 @@-{-# OPTIONS_GHC -Wno-orphans #-}- {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module LWW (lww) where -import Test.QuickCheck (Arbitrary, arbitrary)+import Data.Semigroup ((<>)) import Test.Tasty (TestTree, testGroup)-import Test.Tasty.QuickCheck (Positive (..), testProperty)--import CRDT.Cm (update)-import CRDT.LWW (LWW (Write), point, query, write)+import Test.Tasty.QuickCheck (testProperty) -import Laws (cmrdtLaws, cvrdtLaws)+import CRDT.Cm.LWW (LWW (..))+import CRDT.Cv.LWW (assign, initial, query)+import LamportClock (barrier, runLamportClock, runProcess) -instance Arbitrary a => Arbitrary (LWW a) where- arbitrary = Write <$> arbitrary <*> arbitrary+import Laws (cmrdtLaw, cvrdtLaws) lww :: TestTree lww = testGroup "LWW"- [ testGroup "Cm"- [ cmrdtLaws @(LWW Int)- , testProperty "write latter" $- \formerTime (formerValue :: Int) (Positive dt) latterValue -> let- latterTime = formerTime + dt- state = point formerTime formerValue- state' = update (Write latterTime latterValue) state- in- query state' == latterValue- , testProperty "write former" $- \formerTime (formerValue :: Int) (Positive dt) latterValue -> let- latterTime = formerTime + dt- state = point latterTime latterValue- state' = update (Write formerTime formerValue) state- in- query state' == latterValue- ]+ [ cmrdtLaw @(LWW Int) , testGroup "Cv" [ cvrdtLaws @(LWW Int)- , testProperty "write latter" $- \formerTime (formerValue :: Int) (Positive dt) latterValue -> let- latterTime = formerTime + dt- state = point formerTime formerValue- state' = write latterTime latterValue state- in- query state' == latterValue- , testProperty "write former" $- \formerTime (formerValue :: Int) (Positive dt) latterValue -> let- latterTime = formerTime + dt- state = point latterTime latterValue- state' = write formerTime formerValue state- in- query state' == latterValue+ , 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 ] ]
test/Laws.hs view
@@ -1,30 +1,34 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module Laws- ( cmrdtLaws+ ( cmrdtLaw , cvrdtLaws- , semigroupLaw- , semilatticeLaws ) where +import Control.Monad (unless)+import Data.Function ((&))+import Data.Observe (Observe (..)) import Data.Semigroup (Semigroup, (<>))-import Data.Semilattice (Semilattice, slappend)-import Test.QuickCheck (Arbitrary)+import Data.Semilattice (Semilattice, merge) import Test.Tasty (TestTree, testGroup)-import Test.Tasty.QuickCheck (testProperty)+import Test.Tasty.QuickCheck (Arbitrary (..), discard, testProperty,+ (===), (==>)) -import CRDT.Cm (CmRDT, State, update)+import CRDT.Cm (CmRDT (..), concurrent) import CRDT.Cv (CvRDT)+import LamportClock (Clock, runLamportClock, runProcess) +import ArbitraryOrphans ()+ semigroupLaw :: forall a . (Arbitrary a, Semigroup a, Eq a, Show a) => TestTree semigroupLaw = testGroup "Semigroup law" [testProperty "associativity" associativity] where- associativity :: a -> a -> a -> Bool- associativity x y z = (x <> y) <> z == x <> (y <> z)+ associativity (x, y, z :: a) = (x <> y) <> z == x <> (y <> z) semilatticeLaws :: forall a . (Arbitrary a, Semilattice a, Eq a, Show a) => TestTree@@ -34,23 +38,29 @@ , testProperty "idempotency" idempotency ] where- commutativity :: a -> a -> Bool- commutativity x y = x `slappend` y == y `slappend` x-- idempotency :: a -> Bool- idempotency x = x `slappend` x == x+ commutativity (x, y :: a) = x `merge` y === y `merge` x+ idempotency (x :: a) = x `merge` x === x cvrdtLaws :: forall a . (Arbitrary a, CvRDT a, Eq a, Show a) => TestTree cvrdtLaws = semilatticeLaws @a -cmrdtLaws- :: forall op- . ( Arbitrary op, CmRDT op, Show op- , Arbitrary (State op), Eq (State op), Show (State op)+cmrdtLaw+ :: forall payload op up+ . ( CmRDT payload op up+ , Arbitrary payload, Show payload+ , Arbitrary op, Show op+ , Show (Observed payload) ) => TestTree-cmrdtLaws = testProperty "CmRDT law: commutativity" commutativity+cmrdtLaw = testProperty "CmRDT law: concurrent ops commute" $+ \pid (state0 :: payload) (op1, op2 :: op) ->+ 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 ==> observe state12 === observe state21 where- commutativity :: op -> op -> State op -> Bool- commutativity op1 op2 x =- (update op1 . update op2) x == (update op2 . update op1) x+ updateAtSourceIfCan :: Clock m => op -> payload -> m up+ updateAtSourceIfCan op state =+ unless (updateAtSourcePre op state) discard *> updateAtSource op
test/Main.hs view
@@ -1,9 +1,13 @@ import Test.Tasty (defaultMain, testGroup) +import Counter (counter) import GCounter (gCounter)+import GSet (gSet) import LWW (lww)+import Max (maxTest) import PNCounter (pnCounter)-import GSet (gSet)+import TPSet (tpSet) main :: IO ()-main = defaultMain $ testGroup "" [gCounter, gSet, lww, pnCounter]+main = defaultMain $+ testGroup "" [counter, gCounter, gSet, lww, maxTest, pnCounter, tpSet]
+ test/Max.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Max+ ( maxTest+ ) where++import Test.Tasty (TestTree, testGroup)+import Test.Tasty.QuickCheck (testProperty)++import CRDT.Cv.Max (Max, point, 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/PNCounter.hs view
@@ -1,5 +1,3 @@-{-# OPTIONS_GHC -Wno-orphans #-}- {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -7,40 +5,21 @@ ( pnCounter ) where -import Test.QuickCheck (Arbitrary, arbitrary, arbitraryBoundedEnum) import Test.Tasty (TestTree, testGroup) import Test.Tasty.QuickCheck (testProperty) -import CRDT.Cm (update)-import qualified CRDT.PNCounter.Cm as Cm-import qualified CRDT.PNCounter.Cv as Cv-import qualified CRDT.PNCounter.Cv.Internal as Cv+import CRDT.Cv.PNCounter (PNCounter (..), decrement, increment, query) import GCounter ()-import Laws (cmrdtLaws, cvrdtLaws)--instance Arbitrary (Cm.PNCounter a) where- arbitrary = arbitraryBoundedEnum--instance Arbitrary a => Arbitrary (Cv.PNCounter a) where- arbitrary = Cv.PNCounter <$> arbitrary <*> arbitrary+import Laws (cvrdtLaws) pnCounter :: TestTree pnCounter = testGroup "PNCounter"- [ testGroup "Cv"- [ cvrdtLaws @(Cv.PNCounter Int)- , testProperty "increment" $- \(counter :: Cv.PNCounter Int) i ->- Cv.query (Cv.increment i counter) == succ (Cv.query counter)- , testProperty "decrement" $- \(counter :: Cv.PNCounter Int) i ->- Cv.query (Cv.decrement i counter) == pred (Cv.query counter)- ]- , testGroup "Cm"- [ cmrdtLaws @(Cm.PNCounter Int)- , testProperty "increment" $- \(counter :: Int) -> update Cm.Increment counter == succ counter- , testProperty "decrement" $- \(counter :: Int) -> update Cm.Decrement counter == pred counter- ]+ [ 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/TPSet.hs view
@@ -0,0 +1,13 @@+{-# 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)]