Skip to content

Commit a218917

Browse files
Add MoneyEvents, AccountState and functions to operate on it
1 parent 1cba325 commit a218917

File tree

1 file changed

+96
-0
lines changed

1 file changed

+96
-0
lines changed

packages/spec-haskell/pkgs/semantic-money/src/Money/Theory/SemanticMoney.hs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{-# LANGUAGE DerivingStrategies #-}
22
{-# LANGUAGE FunctionalDependencies #-}
33
{-# LANGUAGE TypeFamilyDependencies #-}
4+
{-# LANGUAGE UndecidableInstances #-}
45

56
module Money.Theory.SemanticMoney where
67

78
import Data.Default (Default (..))
89
import Data.Kind (Type)
10+
import Data.List
911

1012

1113
-- | Type system trite: types used in semantic money
@@ -238,3 +240,97 @@ instance ( MonetaryTypes mt, t ~ MT_TIME mt, v ~ MT_VALUE mt, u ~ MT_UNIT mt, f
238240
(r', er') = if u' == 0 then (0, r `mt_fr_mul_u` u) else r `mt_fr_mul_u_qr_u` (u, u')
239241
b' = fst . flow1 r' $ b
240242
a' = fst . flow1 (er' + flowRate a) $ a
243+
244+
-- Represents a single snapshot of change
245+
data MoneyEvent mt = MoneyEvent
246+
{ eventTime :: MT_TIME mt
247+
, eventValueDelta :: MT_VALUE mt -- Discrete value change
248+
, eventFlowDelta :: MT_FLOWRATE mt -- Rate change
249+
, eventIndicator :: Int -- For conflict resolution
250+
}
251+
252+
-- Representation of an accuont
253+
data AccountState mt = AccountState
254+
{ asTime :: MT_TIME mt
255+
, asValue :: MT_VALUE mt
256+
, asFlowRate :: MT_FLOWRATE mt
257+
}
258+
259+
-- Make the types be Eq compatible
260+
instance ( MonetaryTypes mt
261+
, Eq (MT_TIME mt)
262+
, Eq (MT_VALUE mt)
263+
, Eq (MT_FLOWRATE mt)
264+
) => Eq (MoneyEvent mt) where
265+
a == b = eventTime a == eventTime b
266+
&& eventValueDelta a == eventValueDelta b
267+
&& eventFlowDelta a == eventFlowDelta b
268+
&& eventIndicator a == eventIndicator b
269+
270+
(/=) a b = not (a == b)
271+
272+
instance ( MonetaryTypes mt
273+
, Eq (MT_TIME mt)
274+
, Eq (MT_VALUE mt)
275+
, Eq (MT_FLOWRATE mt)
276+
) => Eq (AccountState mt) where
277+
a == b = asTime a == asTime b
278+
&& asValue a == asValue b
279+
&& asFlowRate a == asFlowRate b
280+
281+
(/=) a b = not (a == b)
282+
283+
-- Takes two AccountState and provides their delta as a MoneyEvent
284+
deltaEvents :: MonetaryTypes mt
285+
=> AccountState mt -- Previous state
286+
-> AccountState mt -- Current state
287+
-> Maybe (MoneyEvent mt)
288+
deltaEvents prev current
289+
| asTime prev >= asTime current = Nothing
290+
| otherwise = Just $ MoneyEvent
291+
{ eventTime = asTime current
292+
, eventValueDelta = asValue current - (asValue prev + valueGrowth)
293+
, eventFlowDelta = asFlowRate current - asFlowRate prev
294+
, eventIndicator = 0
295+
}
296+
where
297+
deltaT = asTime current - asTime prev
298+
valueGrowth = asFlowRate prev `mt_fr_mul_t` fromIntegral deltaT
299+
300+
-- Ensure that the snapshots are conflict-free
301+
ensureSnapshots :: (MonetaryTypes mt, Ord (MT_TIME mt))
302+
=> [MoneyEvent mt]
303+
-> Either String [MoneyEvent mt]
304+
ensureSnapshots snaps = do
305+
let sorted = sortOn eventTime snaps
306+
validated <- validateTimeline sorted
307+
pure $ settleSnapshots validated
308+
309+
-- These two functions below are not required as of now, but they are
310+
-- useful for any real usage of MoneyEvents, since the logic is to
311+
-- have an ordering and operate on the snapshots
312+
313+
-- Ensure that the set of snapshots is strictly ordered by timeline
314+
validateTimeline :: (MonetaryTypes mt, Ord (MT_TIME mt))
315+
=> [MoneyEvent mt]
316+
-> Either String [MoneyEvent mt]
317+
validateTimeline [] = Right []
318+
validateTimeline [x] = Right [x]
319+
validateTimeline (x:y:xs)
320+
| eventTime x > eventTime y = Left "Snapshots out of temporal order"
321+
| eventTime x == eventTime y && eventIndicator x >= eventIndicator y =
322+
Left "Conflicting snapshots: same timestamp with non-decreasing nonce"
323+
| otherwise = (x:) <$> validateTimeline (y:xs)
324+
325+
-- Merge and settle changes of snapshots that are in the same timeline
326+
settleSnapshots :: MonetaryTypes mt => [MoneyEvent mt] -> [MoneyEvent mt]
327+
settleSnapshots = map mergeGroup . groupBy sameTime
328+
where
329+
sameTime a b = eventTime a == eventTime b
330+
mergeGroup [] = error "Empty group"
331+
mergeGroup grp@(x:_) = MoneyEvent
332+
{ eventTime = eventTime x
333+
, eventValueDelta = sum (map eventValueDelta grp)
334+
, eventFlowDelta = sum (map eventFlowDelta grp)
335+
, eventIndicator = maximum (map eventIndicator grp)
336+
}

0 commit comments

Comments
 (0)