packages feed

exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Algebra/Posting.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE RoleAnnotations #-}

-- | Build checked postings at the algebra entry boundary.
-- The module uses only the foundation algebra (@Alg@, @Hat@, @HatBaseClass@)
-- and no accounting semantics. It validates values once and constructs
-- @Posting@ values without implicit cancellation. Models return these postings,
-- and simulators attach notes when committing them.
--
-- Posting construction follows Definitions 3-5.
--
-- @
-- Question                         Operation
-- Check a scalar value             posted
-- Build a posting                  entry
-- Read the underlying algebra      postingAlg
-- @
--
-- For the 'Alg' returned by 'postingAlg', import
-- "ExchangeAlgebra.Algebra.Core" and choose its operations by question:
--
-- @
-- Question                         Core operation
-- Build an algebra                 .@, .+
-- Cancel opposing values           bar
-- Select bases                     proj, projNetNorm
-- Measure values                   norm, balanceBy
-- @
--
-- Import "ExchangeAlgebra.Accounting.Account" alongside this module when
-- the posting base uses account titles. The old "ExchangeAlgebra.Posting"
-- path re-exports the same types and functions during the transition.
module ExchangeAlgebra.Algebra.Posting
    ( -- * Posting values
      Posted
    , PostedError(..)
    , posted
    , unPosted
      -- * Posting sides
    , PostSide(..)
    , sideHat
      -- * Postings
    , Posting
    , entry
    , postingAlg
    ) where

import Control.DeepSeq (NFData(..))
import Data.Binary (Binary(..))
import Data.Hashable (Hashable(..))
import GHC.Generics (Generic)

import ExchangeAlgebra.Algebra.Core.Representation (Alg(Zero), Redundant((.+)), (.@))
import ExchangeAlgebra.Algebra.Value.Class
    (HatVal(isZeroValue, postingUpperBound, valueError, zeroValue), ValueError)
import ExchangeAlgebra.Algebra.Base.Representation
    ( Hat(..)
    , HatBaseClass(BasePart, merge, base)
    )

-- * Posting values

-- | A checked posting value of value type @v@, with no 'Num', 'Fractional',
-- or 'Real' instance (0.6.0.0: polymorphic; 0.5.x fixed @v = Double@).
--
-- Invariant: @valueError x == Nothing@ and, when @postingUpperBound == Just u@,
-- @x <= u@; zero is stored as 'zeroValue' (positive zero for 'Double').
-- Construct values through 'posted'.
--
-- The value type is nominal: @coerce@ cannot turn a value checked at one type
-- into a t'Posted' of another type (for example a user newtype with a smaller
-- 'postingUpperBound').
type role Posted nominal
newtype Posted v = Posted v
    deriving stock (Eq, Ord, Show)

instance NFData v => NFData (Posted v) where
    rnf (Posted value) = rnf value

instance Hashable v => Hashable (Posted v) where
    hashWithSalt salt (Posted value) = hashWithSalt salt value

-- | Decoding checks the same invariant as 'posted' and fails on invalid input.
-- The encoding of @Posted Double@ is the 0.5.x encoding.
instance (HatVal v, Binary v) => Binary (Posted v) where
    put = put . unPosted
    get = do
        value <- get
        case posted value of
            Left failure    -> fail (show failure)
            Right validated -> pure validated

-- | The first failed check, in the order used by 'posted'.
-- (0.6.0.0: the value checks moved to 'ValueError'; 0.5.x had @NonFinite@ and
-- @Negative@ here.)
data PostedError
    = InvalidValue ValueError -- ^ 'valueError' rejected the value.
    | AboveBound              -- ^ A valid value above 'postingUpperBound'.
    deriving stock (Eq, Show, Generic)

instance NFData PostedError

-- | Validate a value, normalizing zero to 'zeroValue' (negative zero to
-- positive zero for 'Double'). Checks 'valueError' and then
-- 'postingUpperBound', returning the corresponding 'PostedError' on failure.
--
-- For every @x@ with @valueError x == Nothing@ and @x <= u@ whenever
-- @postingUpperBound == Just u@, with @normalizeZero x@ equal to 'zeroValue'
-- when @isZeroValue x@ and to @x@ otherwise:
--
-- > fmap unPosted (posted x) == Right (normalizeZero x)
--
-- The law uses exact equality of the value type. The bound is checked only at
-- the posting entry point; '.@' checks 'valueError' alone.
-- Complexity: O(1).
posted :: HatVal v => v -> Either PostedError (Posted v)
posted value
    | Just failure <- valueError value = Left (InvalidValue failure)
    | Just bound <- postingUpperBound
    , value > bound                    = Left AboveBound
    | isZeroValue value                = Right (Posted zeroValue)
    | otherwise                        = Right (Posted value)

-- | Read a checked value. For every @p@, @posted (unPosted p) == Right p@.
-- Complexity: O(1).
unPosted :: Posted v -> v
unPosted (Posted value) = value

-- * Posting sides

-- | The two posting sides; query wildcard 'HatNot' is excluded.
data PostSide
    = HatSide -- ^ The @Hat@ side.
    | NotSide -- ^ The 'Not' side.
    deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)

instance Binary PostSide

instance Hashable PostSide

instance NFData PostSide

-- | Embed a posting side into a query-capable hat.
-- @sideHat HatSide == Hat@ and @sideHat NotSide == Not@; 'HatNot' is never returned.
-- Complexity: O(1).
sideHat :: PostSide -> Hat
sideHat HatSide = Hat
sideHat NotSide = Not

-- * Postings

-- | Postings built through 'entry' and 'Monoid', without implicit cancellation
-- or compression. Equality and display delegate to the underlying 'Alg'.
-- Equality is structural and depends on construction order. To compare
-- multisets of postings instead, compare the results of @toASCList . postingAlg@.
-- (0.6.0.0: polymorphic in the value type @v@; 0.5.x fixed @v = Double@.)
-- The value type is nominal for the same reason as in t'Posted'.
type role Posting nominal nominal
newtype Posting v b = Posting (Alg v b)

instance (HatVal v, HatBaseClass b) => Eq (Posting v b) where
    Posting left == Posting right = left == right

instance (HatVal v, HatBaseClass b) => Show (Posting v b) where
    showsPrec precedence (Posting algebra) = showsPrec precedence algebra

instance NFData (Posting v b) where
    rnf (Posting algebra) = rnf algebra

-- | Preserve algebra addition:
--
-- > postingAlg (a <> b) == (postingAlg a .+ postingAlg b)
--
-- This law uses 'Alg' equality without a tolerance for every 'HatBaseClass'
-- instance, so it also preserves the multiset of postings. Associativity
-- holds as equality of posting multisets, using
-- @sameMultiset x y = toASCList x == toASCList y@:
--
-- > sameMultiset (postingAlg ((a <> b) <> c)) (postingAlg (a <> (b <> c)))
--
-- Complexity: the same as '(.+)' on the underlying algebras.
instance (HatVal v, HatBaseClass b) => Semigroup (Posting v b) where
    Posting left <> Posting right = Posting (left .+ right)

-- | The empty posting obeys @postingAlg mempty == Zero@ using exact 'Alg' equality.
-- The identity laws hold as equality of posting multisets for every
-- 'HatBaseClass' instance, without a numeric tolerance:
--
-- > sameMultiset (postingAlg (mempty <> a)) (postingAlg a)
-- > sameMultiset (postingAlg (a <> mempty)) (postingAlg a)
--
-- Here @sameMultiset x y = toASCList x == toASCList y@.
-- Complexity: O(1) for 'mempty'; combination uses the 'Semigroup' instance.
instance (HatVal v, HatBaseClass b) => Monoid (Posting v b) where
    mempty = Posting Zero

-- | Read the underlying algebra. For every side, checked value, and base part:
--
-- > postingAlg (entry side value part) == unPosted value .@ merge (sideHat side) part
--
-- This is a one-way conversion with exact 'Alg' equality. For a list @xs@
-- of @(side, value, part)@ triples, let @mk (s, v, p) = entry s v p@ and
-- @sameMultiset x y = toASCList x == toASCList y@. Conversion and projection
-- preserve the multiset of postings for every query list @qs@, including
-- 'HatNot' and coordinate wildcards:
--
-- > sameMultiset (postingAlg (foldMap mk xs))
-- >              (foldr (.+) Zero (map (postingAlg . mk) xs))
-- > sameMultiset (proj qs (postingAlg (foldMap mk xs)))
-- >              (foldr (.+) Zero [proj qs (postingAlg (mk x)) | x <- xs])
--
-- These laws use exact multiset equality without a numeric tolerance for
-- every 'HatBaseClass' instance. Complexity of 'postingAlg': O(1).
postingAlg :: Posting v b -> Alg v b
postingAlg (Posting algebra) = algebra

-- | Build one posting from a side, checked value, and base coordinates.
-- A zero value produces 'Zero' through '(.@)'. The side is embedded by
-- 'sideHat' and the coordinates are combined with 'merge'.
--
-- > postingAlg (entry side value part) == unPosted value .@ merge (sideHat side) part
--
-- The law holds with exact 'Alg' equality for every 'HatBaseClass' instance.
-- Complexity: O(1).
entry :: (HatVal v, HatBaseClass b) => PostSide -> Posted v -> BasePart b -> Posting v b
entry side value part = Posting (unPosted value .@ merge (sideHat side) part)