-
Notifications
You must be signed in to change notification settings - Fork 5
/
EventSourcingPattern.hs
150 lines (126 loc) · 6.77 KB
/
EventSourcingPattern.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module EventSourcingPattern where
import Data.Text.Lazy (Text, pack)
{-| We embrace type system and Type class pattern that's why we type all primitive types of our system-}
newtype Product = Product
{ productVal :: String
} deriving (Show, Eq)
newtype Price = Price
{ priceVal :: Double
} deriving (Show, Eq)
newtype Discount = Discount
{ discountVal :: Double
} deriving (Show, Eq)
{-| To define this example we will use the typical grocery basket to be rehydrate -}
data Basket = Basket
{ products :: [Product]
, totalPrice :: Price
, totalDiscount :: Discount
} deriving (Show, Eq)
{-| Data type to define all possible events-}
data Event
= BasketCreated { basket :: Basket }
| ProductAdded { product :: Product
, price :: Price }
| ProductRemoved { product :: Product
, price :: Price }
| DiscountAdded { discount :: Discount }
deriving (Show, Eq)
{-| COMMANDS -}
{-| ------------}
{-| In Event sourcing pattern [Commands] are the responsible to apply an action that lead into the creation of Events.
Those events are the agents that can allow us to rehydrate an entity into a particular state in the future.-}
{-| Command function apply the command and create event [BasketCreated] to be rehydrate later on-}
createBasketCommand :: IO Event
createBasketCommand = return $ BasketCreated $ Basket [] (Price 0) (Discount 0)
{-| Command function apply the command and create event [ProductAdded] to be rehydrate later on-}
addProductCommand :: Basket -> Product -> Price -> IO Event
addProductCommand basket product price = return $ ProductAdded product price
{-| Command function apply the command and create event [ProductRemoved] to be rehydrate later on-}
removeProductCommand :: Basket -> Product -> Price -> IO Event
removeProductCommand basket product price = return $ ProductRemoved product price
{-| Command function apply the command and create event [DiscountAdded] to be rehydrate later on-}
addDiscountCommand :: Basket -> Discount -> IO Event
addDiscountCommand basket discount = return $ DiscountAdded discount
{-| EVENTS -}
{-| ------------}
{-| In Event sourcing pattern [Events] are the events/actions that after apply into an entity, it set in a particular state-}
{-| Declarative function for all Events implementations-}
applyEvent :: Basket -> Event -> Basket
{-| Pure function to create an empty basket-}
applyEvent basket (BasketCreated _basket) = basket
{-| Pure function to create a new Basket adding a product in basket-}
applyEvent basket (ProductAdded product price) =
Basket (products basket ++ [product]) (increaseAmount basket price) (totalDiscount basket)
{-| Pure function to create a new Basket adding a discount in basket-}
applyEvent basket (DiscountAdded discount) =
Basket (products basket) (reduceAmount basket discount) (increaseAmount basket discount)
{-| Pure function to create a new Basket removing the product and price from previous basket-}
applyEvent basket (ProductRemoved product price) =
Basket (removeProductFromBasket product (products basket)) (reduceAmount basket price) (totalDiscount basket)
{-| PERSISTENCE/REHYDRATE -}
{-| ------------------------}
{-| Function that return a new [Event] appending the new event.
This function It could be whatever backend (cassandra, couchbase, mongodb) we want to use to persist -}
appendEvent :: [Event] -> Event -> IO [Event]
appendEvent events event = return $ events ++ [event]
{-| Fold Function to receive the array of events and recursively apply the function [applyEvent] over the basket
depending the type of Event is passed.-}
rehydrateByEvents :: Basket -> [Event] -> Basket
rehydrateByEvents basket (event:events) = rehydrateByEvents (applyEvent basket event) events
rehydrateByEvents basket [] = basket
--rehydrateByEvents basket events = foldl applyEvent basket events --> Using foldl we can reach same result
{-| Utils -}
{-| --------}
{-| Function to filter from the list of products the one we want to remove.
Thanks to eta reduce we dont have to specify the list of products after the filter o the input value of the lamda.-}
removeProductFromBasket :: Product -> [Product] -> [Product]
removeProductFromBasket product = filter (/= product)
{-| Type class pattern implementation to define two implementation with same signature for Price/Discount reducing a lot of boilerplate-}
class ReduceAmount basket moneyToReduce newMoney where
reduceAmount :: basket -> moneyToReduce -> newMoney
instance ReduceAmount Basket Price Price where
reduceAmount basket price = Price $ priceVal (totalPrice basket) - priceVal price
instance ReduceAmount Basket Discount Price where
reduceAmount basket discount = Price $ priceVal (totalPrice basket) - discountVal discount
class IncreaseAmount basket amountToIncrease amount where
increaseAmount :: basket -> amountToIncrease -> amount
instance IncreaseAmount Basket Price Price where
increaseAmount basket price = Price $ priceVal (totalPrice basket) + priceVal price
instance IncreaseAmount Basket Discount Discount where
increaseAmount basket discount = Discount $ discountVal (totalDiscount basket) + discountVal discount
{-| Program -}
{-| ------------}
eventSourcingProgram :: IO ()
eventSourcingProgram = do
print "############### PERSISTANCE COMMANDS #################"
event <- createBasketCommand
events <- appendEvent [] event
event <- addProductCommand (basket event) (Product "Coca-cola") (Price 2.5)
events <- appendEvent events event
event <- addProductCommand (basket event) (Product "Buddbeiser") (Price 3.0)
events <- appendEvent events event
event <- addDiscountCommand (basket event) (Discount 0.5)
events <- appendEvent events event
event <- addProductCommand (basket event) (Product "Nachos") (Price 1.2)
events <- appendEvent events event
event <- addDiscountCommand (basket event) (Discount 0.2)
events <- appendEvent events event
event <- addProductCommand (basket event) (Product "Pepsi") (Price 2.4)
events <- appendEvent events event
event <- removeProductCommand (basket event) (Product "Coca-cola") (Price 2.5)
events <- appendEvent events event
mapM_ print events
print "############### REHYDRATE EVENTS #################"
let basket = rehydrateByEvents (Basket [] (Price 0) (Discount 0)) events
printBasket basket
{-| Function to unbox primitive types from the Types to make it more readable for our consumers-}
printBasket :: Basket -> IO ()
printBasket basket = do
totalPrice <- return (priceVal (totalPrice basket))
print $ "TOTAL PRICE: " ++ (show totalPrice)
totalDiscount <- return (discountVal (totalDiscount basket))
print $ "TOTAL DISCOUNT: " ++ (show totalDiscount)
products <- return $ map (\product -> (productVal product)) (products basket)
print $ "PRODUCTS: " ++ (show products)