liveBook · Manning

Typed-untyped design pattern

As explained in Functional Design and Architecture: Examples in Haskell

The Typed–Untyped design pattern separates a typed abstraction from an untyped representation or boundary to enable wrapping a complete subsystem abstraction.

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

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Explains serialization, runtime storage, typed avatars, and interpreter conversion, but leaves some implementation complexity and unsafe conversion risks to the full code.
Examples
Demonstrates Sentry field serialization and StateVar storage over a runtime map of TVar Any values.
What it assumes
Assumes typed interfaces and interpreters, while introducing heterogeneous storage and typed avatars through logging and STM examples.
What you can do after
Build it from scratch by defining typed operations, hiding heterogeneous runtime storage, and implementing an interpreter that restores each operation’s expected type.
How it connects
Connects typed interfaces with free monads, structured logging, STM, interpreters, concurrency, and resource-independent subsystem abstraction.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Design and Architecture: Examples in Haskell. The section links point to the parts of the book it draws on.