packages feed

crdt (empty) → 0.1

raw patch · 15 files changed

+453/−0 lines, 15 filesdep +QuickCheckdep +basedep +crdt

Dependencies added: QuickCheck, base, crdt, derive, tasty, tasty-quickcheck, vector

Files

+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2017 Yuriy Syrovetskiy++Redistribution and use in source and binary forms, with or without modification,+are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+   this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors+   may be used to endorse or promote products derived from this software without+   specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ crdt.cabal view
@@ -0,0 +1,57 @@+-- This file has been generated from package.yaml by hpack version 0.17.0.+--+-- see: https://github.com/sol/hpack++name:           crdt+version:        0.1+synopsis:       Conflict-free replicated data types+description:    Definitions of CmRDT and CvRDT. Implementations for some classic CRDTs.+category:       Distributed Systems+homepage:       https://github.com/cblp/crdt#readme+bug-reports:    https://github.com/cblp/crdt/issues+maintainer:     Yuriy Syrovetskiy <cblp@cblp.su>+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++source-repository head+  type: git+  location: https://github.com/cblp/crdt++library+  hs-source-dirs:+      lib+  build-depends:+      base >= 4.9 && < 4.10+    , vector+  exposed-modules:+      CRDT.Cm+      CRDT.Cv+      CRDT.GCounter.Cm+      CRDT.GCounter.Cv+      CRDT.GCounter.Cv.Internal+      CRDT.LWW+      CRDT.PNCounter.Cm+      CRDT.PNCounter.Cv+      CRDT.PNCounter.Cv.Internal+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+      test+  build-depends:+      base >= 4.9 && < 4.10+    , vector+    , derive+    , QuickCheck+    , tasty+    , tasty-quickcheck+    , crdt+  other-modules:+      Instances+      Instances.Cm+      Instances.Cv+  default-language: Haskell2010
+ lib/CRDT/Cm.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE TypeFamilies #-}++module CRDT.Cm+    ( CmRDT (..)+    , query+    ) where++import Data.Kind (Type)++{- |+Operation-based, or commutative (Cm) replicated data type.++[Commutativity law]++    @'update' op1 . 'update' op2 == 'update' op2 . 'update' op1@+-}+class CmRDT op where++    -- | The type of the target value+    type State op :: Type++    -- | Apply operation to a value+    update :: op -> State op -> State op++-- | Build state from a series of operations.+query :: (Foldable f, CmRDT op) => f op -> State op -> State op+query ops initial = foldr update initial ops
+ lib/CRDT/Cv.hs view
@@ -0,0 +1,25 @@+module CRDT.Cv+    ( CvRDT+    ) where++import Data.Semigroup (Semigroup (..))++{- |+State-based, or convergent (Cv) replicated data type.++Update is any function modifying @state@.++Query function is not needed. State itself is exposed.+In other words, @query = 'id'@.++Laws:++[commutativity]++    @x '<>' y == y '<>' x@++[idempotency]++    @x '<>' x == x@+-}+class Semigroup state => CvRDT state
+ lib/CRDT/GCounter/Cm.hs view
@@ -0,0 +1,21 @@+{-# 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++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 view
@@ -0,0 +1,37 @@+module CRDT.GCounter.Cv+    ( GCounter+    , increment+    , initial+    , query+    ) where++import           Data.Monoid         ((<>))+import qualified Data.Vector         as Vector+import qualified Data.Vector.Mutable as VectorM++import CRDT.GCounter.Cv.Internal++-- | Increment counter+increment+    :: Num a+    => Word -- ^ replica id+    -> GCounter a+    -> GCounter a+increment replicaId (GCounter vec) = let+    i = fromIntegral replicaId+    vecResized =+        if i + 1 > length vec then+            vec <> Vector.replicate (i + 1 - length vec) 0+        else+            vec+    vecUpdated = Vector.modify (\vm -> VectorM.modify vm (+1) i) vecResized+    in+    GCounter vecUpdated++-- | Initial state+initial :: GCounter a+initial = GCounter Vector.empty++-- | Get value from the state+query :: Num a => GCounter a -> a+query (GCounter v) = sum v
+ lib/CRDT/GCounter/Cv/Internal.hs view
@@ -0,0 +1,16 @@+module CRDT.GCounter.Cv.Internal where++import           Data.Semigroup (Semigroup ((<>)))+import           Data.Vector    (Vector)+import qualified Data.Vector    as Vector++import CRDT.Cv (CvRDT)++-- | Grow-only counter.+newtype GCounter a = GCounter (Vector a)+    deriving Eq++instance Ord a => Semigroup (GCounter a) where+    GCounter x <> GCounter y = GCounter $ Vector.zipWith max x y++instance Ord a => CvRDT (GCounter a)
+ lib/CRDT/LWW.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE TypeFamilies #-}++module CRDT.LWW+    ( LWW (..)+    , Timestamp+    ) where++import Data.Semigroup  (Semigroup (..))+import Numeric.Natural (Natural)++import           CRDT.Cm (CmRDT, State)+import qualified CRDT.Cm as Cm+import           CRDT.Cv (CvRDT)++type Timestamp = Natural++-- | Last write wins. Interesting, this type is both 'CmRDT' and 'CvRDT'.+data LWW a = Write+    { timestamp :: !Timestamp+    , value     :: !a+    }+    deriving (Eq, Ord)++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)
+ lib/CRDT/PNCounter/Cm.hs view
@@ -0,0 +1,22 @@+{-# 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++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 view
@@ -0,0 +1,40 @@+{-# LANGUAGE NamedFieldPuns #-}++module CRDT.PNCounter.Cv+    ( PNCounter+    , decrement+    , increment+    , initial+    , query+    ) 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 view
@@ -0,0 +1,22 @@+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++instance Ord a => Semigroup (PNCounter a) where+    PNCounter p1 n1 <> PNCounter p2 n2 = PNCounter (p1 <> p2) (n1 <> n2)++instance Ord a => CvRDT (PNCounter a)
+ test/Instances.hs view
@@ -0,0 +1,4 @@+module Instances () where++import Instances.Cm ()+import Instances.Cv ()
+ test/Instances/Cm.hs view
@@ -0,0 +1,19 @@+{-# OPTIONS_GHC -Wno-orphans #-}++{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}++module Instances.Cm () where++import Data.DeriveTH   (derives, makeArbitrary)+import Test.QuickCheck (Arbitrary, arbitrary, choose)++import CRDT.LWW          (LWW (..))+import CRDT.PNCounter.Cm (PNCounter (..))++derives [makeArbitrary] [''PNCounter, ''LWW]++deriving instance Show (PNCounter a)++deriving instance Show a => Show (LWW a)
+ test/Instances/Cv.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_GHC -Wno-orphans #-}++{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-}++module Instances.Cv () where++import           Data.Vector     (Vector)+import qualified Data.Vector     as Vector+import           Test.QuickCheck (Arbitrary, arbitrary)++import CRDT.GCounter.Cv.Internal  (GCounter (..))+import CRDT.PNCounter.Cv.Internal (PNCounter (..))++instance Arbitrary a => Arbitrary (Vector a) where+    arbitrary = Vector.fromList <$> arbitrary++deriving instance Arbitrary a => Arbitrary (GCounter a)++deriving instance Show a => Show (GCounter a)++instance Arbitrary a => Arbitrary (PNCounter a) where+    arbitrary = PNCounter <$> arbitrary <*> arbitrary++deriving instance Show a => Show (PNCounter a)
+ test/Main.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}++import Data.Proxy            (Proxy (..))+import Data.Semigroup        ((<>))+import Test.QuickCheck       (Arbitrary, Small (..))+import Test.Tasty            (TestTree, defaultMain, testGroup)+import Test.Tasty.QuickCheck (testProperty)++import           CRDT.Cm           (CmRDT, State)+import qualified CRDT.Cm           as Cm+import           CRDT.Cv           (CvRDT)+import qualified CRDT.GCounter.Cv  as GcCv+import           CRDT.LWW          (LWW)+import qualified CRDT.PNCounter.Cm as PncCm+import qualified CRDT.PNCounter.Cv as PncCv++import Instances ()++main :: IO ()+main = defaultMain $ testGroup "" [gCounter, pnCounter, lww]++gCounter :: TestTree+gCounter = testGroup "GCounter"+    [ testGroup "Cv"+        [ cvrdtLaws (Proxy :: Proxy (GcCv.GCounter Int))+        , testProperty "increment" $+            \(c :: GcCv.GCounter Int) (Small i) ->+                GcCv.query (GcCv.increment i c) == succ (GcCv.query c)+        ]+    ]++pnCounter :: TestTree+pnCounter = testGroup "PNCounter"+    [ testGroup "Cv"+        [ cvrdtLaws (Proxy :: Proxy (PncCv.PNCounter Int))+        , testProperty "increment" $+            \(c :: PncCv.PNCounter Int) (Small i) ->+                PncCv.query (PncCv.increment i c) == succ (PncCv.query c)+        , testProperty "decrement" $+            \(c :: PncCv.PNCounter Int) (Small i) ->+                PncCv.query (PncCv.decrement i c) == pred (PncCv.query c)+        ]+    , testGroup "Cm"+        [ cmrdtCommutativity (Proxy :: Proxy (PncCm.PNCounter Int)) ]+    ]++lww :: TestTree+lww = testGroup "LWW"+    [ testGroup "Cm" [ cmrdtCommutativity (Proxy :: Proxy (LWW Int)) ]+    , testGroup "Cv" [ cvrdtLaws (Proxy :: Proxy (LWW Int)) ]+    ]++cvrdtLaws+    :: forall a . (Arbitrary a, CvRDT a, Eq a, Show a) => Proxy a -> TestTree+cvrdtLaws _ = testGroup "CvRDT laws"+    [ testProperty "associativity" associativity+    , testProperty "commutativity" commutativity+    , testProperty "idempotency"   idempotency+    ]+  where+    associativity :: a -> a -> a -> Bool+    associativity x y z = (x <> y) <> z == x <> (y <> z)++    commutativity :: a -> a -> Bool+    commutativity x y = x <> y == y <> x++    idempotency :: a -> Bool+    idempotency x = x <> x == x++cmrdtCommutativity+    :: forall op+    . ( Arbitrary op, CmRDT op, Show op+      , Arbitrary (State op), Eq (State op), Show (State op)+      )+    => Proxy op -> TestTree+cmrdtCommutativity _ = testProperty "CmRDT law: commutativity" commutativity+  where+    commutativity :: op -> op -> State op -> Bool+    commutativity op1 op2 x =+        (Cm.update op1 . Cm.update op2) x == (Cm.update op2 . Cm.update op1) x