The Typed–Untyped design pattern separates a typed interface from an untyped implementation, allowing one abstraction to wrap a complete subsystem without exposing representation details. section 10.1.4 section 10.4
The pattern addresses two recurring engineering problems:
- Structured logging exposes typed field constructors while the interpreter serializes heterogeneous fields into service-specific records. section 10.1.4
- Abstract state exposes typed StateVar values while runtime storage keeps heterogeneous TVars behind identifiers and an untyped representation. section 10.2.1
- Typed avatars preserve the caller’s type-level intent even though the interpreter performs the representation conversion internally. section 10.2.1
Wrapping STM state
A StateVar a contains only an integer identifier, while the runtime maps that identifier to a TVar Any. ReadVar accepts StateVar a, retrieves the hidden value, and converts it back for the typed operation. Thus application code can use readVar and writeVar instead of manipulating TVar directly, while the interpreter owns storage and concurrency details. section 10.2.1
Listing 10.11 Runtime and interpreter of the StateL language
data StateRuntime = StateRuntime
{ _varId :: TVar VarId #1
, _state :: TMVar (Map VarId (TVar Any))
}
interpretStateF :: StateRuntime -> StateF r -> STM r #2
interpretStateF stateRt (ReadVar var next) = do
val <- readVar' stateRt var #3
pure (next val)
readVar' :: StateRuntime -> StateVar a -> STM a #4
readVar' stateRt (StateVar varId) = do
nodeState <- readTMVar (_state stateRt)
case Map.lookup varId nodeState of
Nothing -> error ("Var not found: " <> varId)
Just (RVarHandle tvar) -> do
anyVal <- readTVar tvar
let (val :: a) = usafeCoerce anyVal #5
pure a
The runtime and interpreter listing makes the boundary concrete: typed StateVar handles enter an identifier-based map, and the interpreter retrieves values from untyped TVars. section 10.2.1
