keiki-0.1.0.0: src/Keiki/Decider.hs
-- | A Chassaing-shape Decider façade over a 'SymTransducer'.
--
-- Users coming from the naive functional event-sourcing world
-- (Jérémie Chassaing's /Functional Event Sourcing Decider/) work
-- with a small record:
--
-- @
-- data Decider c e s = Decider
-- { decide :: c -> s -> [e]
-- , evolve :: s -> e -> s
-- , initialState :: s
-- , isTerminal :: s -> Bool
-- }
-- @
--
-- 'toDecider' projects the keiki 'SymTransducer' onto this shape.
-- 'decide' is built on 'omega' (the forward step that emits one or
-- more events per command after the EP-19 widening); 'evolve' is
-- built on 'applyEvent' (the letter-only inverse step). With the
-- widened @'Keiki.Core.Edge.output' :: ['Keiki.Core.OutTerm' rs ci
-- co]@ a single command can yield two or more events end-to-end
-- without any state-refinement scaffolding — 'decide' returns the
-- full list directly. EP-19 retired the previous EP-20 façades
-- (@toMultiDecider@ + @DriverConfig@); the multi-event behaviour is
-- now first-class in the AST.
--
-- == Streaming replay through multi-event edges
--
-- The 'evolve' field is letter-only (handles edges with output of
-- length 0 or 1) and remains the canonical letter-replay verb.
-- Event-by-event streaming across a length-2+ edge passes through
-- the intermediate "I just observed event 1, expecting event 2 next"
-- state; the 'evolveStreaming' field exposes this via
-- 'Keiki.Core.InFlight' and 'Keiki.Core.applyEventStreaming'. The
-- two evolve fields agree on length-0/1 commands; they diverge
-- only on length-2+ where the streaming path stays /InFlight/
-- between events.
--
-- == One semantic gap remains
--
-- /ε-edges/ — edges whose @output@ is @[]@. The transducer
-- transitions state without emitting an event. 'decide' returns
-- @[]@ for such commands, and the result is identical to "no event
-- happened" from the Decider record's perspective. Use
-- 'Keiki.Core.delta' / 'Keiki.Core.step' directly when ε-driven
-- transitions matter.
module Keiki.Decider
( Decider (..),
toDecider,
)
where
import Keiki.Core
( BoolAlg,
InFlight (..),
RegFile,
SymTransducer (..),
applyEvent,
applyEventStreaming,
omega,
)
-- | The Chassaing-shape Decider record. Field selectors are named
-- to match published Decider examples; conflicts with other modules
-- are avoided by importing this module qualified.
--
-- The @s_streaming@ parameter carries the InFlight-aware streaming
-- state ('Keiki.Core.InFlight' s co paired with a register file);
-- for letter-only callers it is unused.
data Decider c e s s_streaming = Decider
{ decide :: c -> s -> [e],
evolve :: s -> e -> s,
evolveStreaming :: s_streaming -> e -> Maybe s_streaming,
initialState :: s,
isTerminal :: s -> Bool
}
-- | Project a keiki 'SymTransducer' to a 'Decider' record. The
-- letter-replay state carrier is @(s, RegFile rs)@ and the
-- streaming-replay state carrier is @('Keiki.Core.InFlight' s co,
-- RegFile rs)@ — keiki edge guards depend on the register file as
-- well as the control vertex, and streaming replay through a
-- length-2+ edge intrinsically observes a mid-chain wrapper.
--
-- == Field-by-field correspondence
--
-- @
-- decide d cmd (s, regs) -- = omega t s regs cmd (EP-19 widened)
-- evolve d (s, regs) ev -- = letter-only applyEvent;
-- -- on length-2+ edges, falls back
-- -- to the input state defensively
-- evolveStreaming d ws ev -- = applyEventStreaming, returning
-- -- the wrapped state mid-chain
-- initialState d -- = (initial t, initialRegs t)
-- isTerminal d (s, _regs) -- = isFinal t s
-- @
--
-- == Defensive 'evolve'
--
-- 'applyEvent' returns 'Nothing' when an event cannot be replayed
-- letter-by-letter from @(s, regs)@. To keep the Chassaing signature
-- non-'Maybe', 'evolve' returns the input state on failure. Callers
-- that want strict replay use 'evolveStreaming' (whose 'Maybe' is
-- explicit) or 'Keiki.Core.applyEvents' (which returns 'Nothing' on
-- the first replay failure across a chunk).
toDecider ::
(BoolAlg phi (RegFile rs, ci), Eq co) =>
SymTransducer phi rs s ci co ->
Decider ci co (s, RegFile rs) (InFlight s co, RegFile rs)
toDecider t =
Decider
{ decide = \cmd (s, regs) -> omega t s regs cmd,
evolve = \(s, regs) ev -> case applyEvent t s regs ev of
Just (s', regs') -> (s', regs')
Nothing -> (s, regs),
evolveStreaming = \(w, regs) ev -> applyEventStreaming t w regs ev,
initialState = (initial t, initialRegs t),
isTerminal = \(s, _regs) -> isFinal t s
}