add Action to high-level trigger updateState (#7621)

* add ActionState to the standard library

* use 1 ActionState, 1 get, 1 put in low-level trigger library

* introduce TriggerStateA for updateState

* fix tests and examples for new updateState signature

CHANGELOG_BEGIN
- [Triggers] The ``updateState`` function now returns a ``TriggerStateA``.  This
  is an action like ``TriggerA``, but doesn't permit emitting commands.  Instead
  of taking the state as an argument and returning a new state, you can
  manipulate the state with ``get``, ``put``, and ``modify``.  Any existing
  ``updateState`` can be ported by replacing ``s -> expr`` in the lambda
  expression with ``-> modify $ \s ->``, and then made to look nicer from there
  as desired.
  See `issue #7621 <https://github.com/digital-asset/daml/pull/7621>`__.
CHANGELOG_END

* some DAML docs for updateState and TriggerStateA
This commit is contained in:
Stephen Compall 2020-10-09 13:56:24 -04:00 committed by GitHub
parent 8b9c237031
commit 664a0c0076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 97 additions and 56 deletions

View File

@ -51,7 +51,7 @@ deriving instance Ord Copy
copyTrigger : Trigger () copyTrigger : Trigger ()
copyTrigger = Trigger copyTrigger = Trigger
{ initialize = \_acs -> () { initialize = \_acs -> ()
, updateState = \_acs _message () -> () , updateState = \_acs _message -> pure ()
, rule = copyRule , rule = copyRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -11,6 +11,8 @@ module DA.Action.State
, modify , modify
) where ) where
import DA.Action.State.Class
-- | A value of type `State s a` represents a computation that has access to a state variable -- | A value of type `State s a` represents a computation that has access to a state variable
-- of type `s` and produces a value of type `a`. -- of type `s` and produces a value of type `a`.
-- --
@ -23,6 +25,15 @@ module DA.Action.State
-- >>> execState (modify (+1)) 0 -- >>> execState (modify (+1)) 0
-- 1 -- 1
-- --
-- >>> runState (do x <- get; modify (+1); pure x) 0
-- (0, 1)
--
-- >>> runState (put 1) 0
-- ((), 1)
--
-- >>> runState (modify (+1)) 0
-- ((), 1)
--
-- Note that values of type `State s a` are not serializable. -- Note that values of type `State s a` are not serializable.
newtype State s a = State { runState : s -> (a, s) } newtype State s a = State { runState : s -> (a, s) }
deriving Functor deriving Functor
@ -47,23 +58,7 @@ evalState a s = fst (runState a s)
execState : State s a -> s -> s execState : State s a -> s -> s
execState a s = snd (runState a s) execState a s = snd (runState a s)
-- | Fetch the current value of the state variable. instance ActionState s (State s) where
-- get = State (\s -> (s, s))
-- >>> runState (do x <- get; modify (+1); pure x) 0 put s = State (\_ -> ((), s))
-- (0, 1) modify f = State (\s -> ((), f s))
get : State s s
get = State (\s -> (s, s))
-- | Set the value of the state variable.
--
-- >>> runState (put 1) 0
-- ((), 1)
put : s -> State s ()
put s = State (\_ -> ((), s))
-- | Modify the state variable with the given function.
--
-- >>> runState (modify (+1)) 0
-- ((), 1)
modify : (s -> s) -> State s ()
modify f = State (\s -> ((), f s))

View File

@ -0,0 +1,35 @@
-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE DefaultSignatures #-}
-- | DA.Action.State.Class
module DA.Action.State.Class
( ActionState(..)
) where
-- | Action `m` has a state variable of type `s`.
--
-- Rules:
-- get *> ma = ma
-- ma <* get = ma
-- put a >>= get = put a $> a
-- put a *> put b = put b
-- (,) <$> get <*> get = get <&> \a -> (a, a)
--
-- Informally, these rules mean it behaves like an ordinary assignable variable:
-- it doesn't magically change value by looking at it, if you put a value there
-- that's always the value you'll get if you read it, assigning a value but
-- never reading that value has no effect, and so on.
class ActionState s m | m -> s where
{-# MINIMAL get, (put | modify) #-}
-- | Fetch the current value of the state variable.
get : m s
-- | Set the value of the state variable.
put : s -> m ()
put = modify . const
-- | Modify the state variable with the given function.
modify : (s -> s) -> m ()
default modify : Action m => (s -> s) -> m ()
modify f = put . f =<< get

View File

@ -102,7 +102,7 @@ To create a trigger you need to define a value of type ``Trigger s`` where ``s``
data Trigger s = Trigger data Trigger s = Trigger
{ initialize : ACS -> s { initialize : ACS -> s
, updateState : ACS -> Message -> s -> s , updateState : ACS -> Message -> TriggerStateA s ()
, rule : Party -> ACS -> s -> TriggerA () , rule : Party -> ACS -> s -> TriggerA ()
, registeredTemplates : RegisteredTemplates , registeredTemplates : RegisteredTemplates
, heartbeat : Optional RelTime , heartbeat : Optional RelTime

View File

@ -51,7 +51,7 @@ deriving instance Ord Copy
copyTrigger : Trigger () copyTrigger : Trigger ()
copyTrigger = Trigger copyTrigger = Trigger
{ initialize = \_acs -> () { initialize = \_acs -> ()
, updateState = \_acs _message () -> () , updateState = \_acs _message -> pure ()
, rule = copyRule , rule = copyRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -18,7 +18,7 @@ import UpgradeFromCoinV1
upgradeTrigger : Trigger () upgradeTrigger : Trigger ()
upgradeTrigger = Trigger with upgradeTrigger = Trigger with
initialize = \_acs -> () initialize = \_acs -> ()
updateState = \_acs _msg () -> () updateState = \_acs _msg -> pure ()
registeredTemplates = AllInDar registeredTemplates = AllInDar
heartbeat = None heartbeat = None
rule = triggerRule rule = triggerRule

View File

@ -9,6 +9,10 @@ module Daml.Trigger
, getTemplates , getTemplates
, Trigger(..) , Trigger(..)
, TriggerA , TriggerA
, TriggerStateA
, get
, put
, modify
, emitCommands , emitCommands
, runTrigger , runTrigger
, CommandId , CommandId
@ -35,6 +39,7 @@ module Daml.Trigger
) where ) where
import DA.Action import DA.Action
import DA.Action.State (execState)
import DA.Functor ((<&>)) import DA.Functor ((<&>))
#ifdef DAML_GENMAP && DAML_GENERIC_COMPARISON #ifdef DAML_GENMAP && DAML_GENERIC_COMPARISON
import qualified DA.Map as GMap import qualified DA.Map as GMap
@ -71,9 +76,10 @@ getContracts (ACS tpls pending) = mapOptional fromAny
data Trigger s = Trigger data Trigger s = Trigger
{ initialize : ACS -> s { initialize : ACS -> s
-- ^ Initialize the user-defined state based on the ACS. -- ^ Initialize the user-defined state based on the ACS.
, updateState : ACS -> Message -> s -> s , updateState : ACS -> Message -> TriggerStateA s ()
-- ^ Update the user-defined state based on the ACS and a transaction or -- ^ Update the user-defined state based on the ACS and a transaction
-- completion message. -- or completion message. It can manipulate the state with `get`,
-- `put`, and `modify`.
, rule : Party -> ACS -> s -> TriggerA () , rule : Party -> ACS -> s -> TriggerA ()
-- ^ The rule defines the main logic of your trigger. It can send commands -- ^ The rule defines the main logic of your trigger. It can send commands
-- to the ledger using `emitCommands` to change the ACS. -- to the ledger using `emitCommands` to change the ACS.
@ -196,12 +202,13 @@ runTrigger userTrigger = LowLevel.Trigger
userState = userTrigger.initialize acs userState = userTrigger.initialize acs
state = TriggerState acs party userState Map.empty state = TriggerState acs party userState Map.empty
in TriggerSetup $ execStateT (runTriggerRule $ runRule userTrigger.rule) state in TriggerSetup $ execStateT (runTriggerRule $ runRule userTrigger.rule) state
utUpdateState acs msg = execState $ runTriggerStateA $ userTrigger.updateState acs msg
update msg = do update msg = do
time <- getTime time <- getTime
state <- get state <- get
case msg of case msg of
MCompletion completion -> MCompletion completion ->
let userState = userTrigger.updateState state.acs (MCompletion completion) state.userState let userState = utUpdateState state.acs (MCompletion completion) state.userState
in case completion.status of in case completion.status of
Succeeded {} -> Succeeded {} ->
-- We delete successful completions when we receive the corresponding transaction -- We delete successful completions when we receive the corresponding transaction
@ -214,7 +221,7 @@ runTrigger userTrigger = LowLevel.Trigger
runRule userTrigger.rule runRule userTrigger.rule
MTransaction transaction -> do MTransaction transaction -> do
let acs = applyTransaction transaction state.acs let acs = applyTransaction transaction state.acs
userState = userTrigger.updateState acs (MTransaction transaction) state.userState userState = utUpdateState acs (MTransaction transaction) state.userState
-- See the comment above for why we delete this here instead of when we receive the completion. -- See the comment above for why we delete this here instead of when we receive the completion.
(acs', commandsInFlight) = case transaction.commandId of (acs', commandsInFlight) = case transaction.commandId of
None -> (acs, state.commandsInFlight) None -> (acs, state.commandsInFlight)
@ -222,6 +229,6 @@ runTrigger userTrigger = LowLevel.Trigger
put $ state { acs = acs', userState, commandsInFlight } put $ state { acs = acs', userState, commandsInFlight }
runRule userTrigger.rule runRule userTrigger.rule
MHeartbeat -> do MHeartbeat -> do
let userState = userTrigger.updateState state.acs MHeartbeat state.userState let userState = utUpdateState state.acs MHeartbeat state.userState
put $ state { userState } put $ state { userState }
runRule userTrigger.rule runRule userTrigger.rule

View File

@ -6,6 +6,7 @@
module Daml.Trigger.Internal module Daml.Trigger.Internal
( ACS (..) ( ACS (..)
, TriggerA (..) , TriggerA (..)
, TriggerStateA (..)
, addCommands , addCommands
, insertTpl , insertTpl
, groupActiveContracts , groupActiveContracts
@ -18,6 +19,7 @@ module Daml.Trigger.Internal
, TriggerState (..) , TriggerState (..)
) where ) where
import DA.Action.State
import DA.Next.Map (Map) import DA.Next.Map (Map)
import qualified DA.Next.Map as Map import qualified DA.Next.Map as Map
#ifdef DAML_GENMAP && DAML_GENERIC_COMPARISON #ifdef DAML_GENMAP && DAML_GENERIC_COMPARISON
@ -50,6 +52,14 @@ data ACS = ACS
newtype TriggerA a = TriggerA (TriggerRule TriggerAState a) newtype TriggerA a = TriggerA (TriggerRule TriggerAState a)
deriving (Functor, Applicative, Action, HasTime) deriving (Functor, Applicative, Action, HasTime)
-- | TriggerStateA is the type used in the `updateState` of a DAML
-- trigger. It has similar actions in common with `TriggerA`, but
-- cannot use `emitCommands` or `getTime`.
newtype TriggerStateA s a = TriggerStateA { runTriggerStateA : State s a }
deriving (Functor, Applicative, Action)
deriving instance ActionState s (TriggerStateA s)
-- Internal API -- Internal API
addCommands : Map CommandId [Command] -> Commands -> Map CommandId [Command] addCommands : Map CommandId [Command] -> Commands -> Map CommandId [Command]

View File

@ -39,13 +39,13 @@ module Daml.Trigger.LowLevel
, TriggerSetup(..) , TriggerSetup(..)
, TriggerRule(..) , TriggerRule(..)
, ActionState(..) , ActionState(..)
, modify
, zoom , zoom
, submitCommands , submitCommands
, simulateRule , simulateRule
) where ) where
import qualified DA.Action.State as State import DA.Action.State
import DA.Action.State.Class
import DA.Functor ((<&>)) import DA.Functor ((<&>))
import DA.Next.Map (MapKey(..)) import DA.Next.Map (MapKey(..))
import DA.Time (RelTime(..)) import DA.Time (RelTime(..))
@ -272,16 +272,10 @@ instance Action m => Action (StateT s m) where
(x', s') <- x s (x', s') <- x s
runStateT (f x') s' runStateT (f x') s'
class ActionState s m | m -> s where
get : m s
put : s -> m ()
modify : (Action m, ActionState s m) => (s -> s) -> m ()
modify f = put . f =<< get
instance Applicative m => ActionState s (StateT s m) where instance Applicative m => ActionState s (StateT s m) where
get = StateT $ \s -> pure (s, s) get = StateT $ \s -> pure (s, s)
put s = StateT $ const $ pure ((), s) put s = StateT $ const $ pure ((), s)
modify f = StateT $ \s -> pure ((), f s)
execStateT : Functor m => StateT s m a -> s -> m s execStateT : Functor m => StateT s m a -> s -> m s
execStateT (StateT fa) = fmap snd . fa execStateT (StateT fa) = fmap snd . fa
@ -306,13 +300,13 @@ newtype TriggerRule s a = TriggerRule { runTriggerRule : StateT s (Free TriggerF
-- meant for testing purposes only. -- meant for testing purposes only.
simulateRule : TriggerRule s a -> Time -> s -> (s, [Commands], a) simulateRule : TriggerRule s a -> Time -> s -> (s, [Commands], a)
simulateRule rule time s = (s', reverse cmds, a) simulateRule rule time s = (s', reverse cmds, a)
where ((a, s'), (cmds, _)) = State.runState (foldFree sim (runStateT (runTriggerRule rule) s)) ([], 0) where ((a, s'), (cmds, _)) = runState (foldFree sim (runStateT (runTriggerRule rule) s)) ([], 0)
sim : TriggerF x -> State.State ([Commands], Int) x sim : TriggerF x -> State ([Commands], Int) x
sim (GetTime f) = pure (f time) sim (GetTime f) = pure (f time)
sim (Submit (cmds, f)) = do sim (Submit (cmds, f)) = do
(pastCmds, nextId) <- State.get (pastCmds, nextId) <- get
let nextIdShown = show nextId let nextIdShown = show nextId
State.put (Commands (CommandId nextIdShown) cmds :: pastCmds, nextId + 1) put (Commands (CommandId nextIdShown) cmds :: pastCmds, nextId + 1)
pure $ f nextIdShown pure $ f nextIdShown
deriving instance ActionState s (TriggerRule s) deriving instance ActionState s (TriggerRule s)

View File

@ -8,7 +8,7 @@ import Daml.Trigger
trigger : Trigger () trigger : Trigger ()
trigger = Trigger with trigger = Trigger with
initialize = \_ -> () initialize = \_ -> ()
updateState = \_ _ _ -> () updateState = \_ _ -> pure ()
rule = triggerRule rule = triggerRule
registeredTemplates = AllInDar registeredTemplates = AllInDar
heartbeat = None heartbeat = None

View File

@ -25,7 +25,7 @@ template B
trigger : Trigger () trigger : Trigger ()
trigger = Trigger with trigger = Trigger with
initialize = \_ -> () initialize = \_ -> ()
updateState = \_ _ _ -> () updateState = \_ _ -> pure ()
rule = triggerRule rule = triggerRule
registeredTemplates = AllInDar registeredTemplates = AllInDar
heartbeat = None heartbeat = None

View File

@ -9,7 +9,7 @@ import Daml.Trigger
createAndExerciseTrigger : Trigger () createAndExerciseTrigger : Trigger ()
createAndExerciseTrigger = Trigger createAndExerciseTrigger = Trigger
{ initialize = \_ -> () { initialize = \_ -> ()
, updateState = \_ _ _ -> () , updateState = \_ _ -> pure ()
, rule = createAndExerciseRule , rule = createAndExerciseRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -9,10 +9,10 @@ import Daml.Trigger
exerciseByKeyTrigger : Trigger Int exerciseByKeyTrigger : Trigger Int
exerciseByKeyTrigger = Trigger exerciseByKeyTrigger = Trigger
{ initialize = \_acs -> 3 { initialize = \_acs -> 3
, updateState = \_acs msg allowedFail -> case msg of , updateState = \_acs msg -> case msg of
MCompletion c MCompletion c
| Failed {} <- c.status -> allowedFail - 1 | Failed {} <- c.status -> modify (subtract 1)
_ -> allowedFail _ -> pure ()
, rule = retryRule , rule = retryRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -10,7 +10,7 @@ import Daml.Trigger
maxInboundMessageSizeTrigger : Trigger () maxInboundMessageSizeTrigger : Trigger ()
maxInboundMessageSizeTrigger = Trigger maxInboundMessageSizeTrigger = Trigger
{ initialize = \_ -> () { initialize = \_ -> ()
, updateState = \_ _ _ -> () , updateState = \_ _ -> pure ()
, rule = maxInboundMessageSizeRule , rule = maxInboundMessageSizeRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -36,7 +36,7 @@ template Done
booTrigger : Trigger () booTrigger : Trigger ()
booTrigger = Trigger with booTrigger = Trigger with
initialize = \acs -> () initialize = \acs -> ()
updateState = \acs _ s -> s updateState = \acs _ -> pure ()
rule = booRule rule = booRule
registeredTemplates = AllInDar registeredTemplates = AllInDar
heartbeat = None heartbeat = None

View File

@ -9,10 +9,10 @@ import Daml.Trigger
retryTrigger : Trigger Int retryTrigger : Trigger Int
retryTrigger = Trigger retryTrigger = Trigger
{ initialize = \_acs -> 3 { initialize = \_acs -> 3
, updateState = \_acs msg allowedFail -> case msg of , updateState = \_acs msg -> case msg of
MCompletion c MCompletion c
| Failed {} <- c.status -> allowedFail - 1 | Failed {} <- c.status -> modify (subtract 1)
_ -> allowedFail _ -> pure ()
, rule = retryRule , rule = retryRule
, registeredTemplates = AllInDar , registeredTemplates = AllInDar
, heartbeat = None , heartbeat = None

View File

@ -10,7 +10,7 @@ import Daml.Trigger
test : RegisteredTemplates -> Trigger () test : RegisteredTemplates -> Trigger ()
test registered = Trigger test registered = Trigger
{ initialize = \_acs -> () { initialize = \_acs -> ()
, updateState = \_acs _message () -> () , updateState = \_acs _message -> pure ()
, registeredTemplates = registered , registeredTemplates = registered
, rule = \party acs () -> do , rule = \party acs () -> do
let ones = getContracts @One acs let ones = getContracts @One acs

View File

@ -30,7 +30,7 @@ template T
trigger : Trigger Int trigger : Trigger Int
trigger = Trigger with trigger = Trigger with
initialize = const 0 initialize = const 0
updateState = \acs _msg count -> length (getContracts @T acs) updateState = \acs _msg -> put $ length (getContracts @T acs)
rule = \party acs count -> do rule = \party acs count -> do
when (count == 1) do when (count == 1) do
priorCIF <- getCommandsInFlight priorCIF <- getCommandsInFlight