Exceptions and State
Control.App
is primarily intended to make it easier to manage the common
cases of applications with exceptions and state. We can throw and catch
exceptions listed in the list of errors (the es
parameter to App
) and
introduce new global state.
Exceptions
The List Error
is a list of error types, which can be thrown and caught
using the functions below:
throw : HasErr err es => err -> App es a
catch : HasErr err es => App es a -> (err -> App es a) -> App es a
We can use throw
and catch
for some exception type err
as
long as the exception type exists in the list of errors, es
, as
checked by the HasErr
predicate, also defined in Control.App
(Also, note that Exception
is a synonym for HasErr
):
data HasErr : Error -> List Error -> Type where
Here : HasErr e (e :: es)
There : HasErr e es -> HasErr e (e' :: es)
Exception : Error -> List Error -> Type
Exception = HasErr
Finally, we can introduce new exception types via handle
, which runs a
block of code which might throw, handling any exceptions:
handle : App (err :: e) a ->
(onok : a -> App e b) ->
(onerr : err -> App e b) -> App e b
Adding State
Applications will typically need to keep track of state, and we support
this primitively in App
using a State
type, defined in
Control.App
:
data State : (tag : a) -> Type -> List Error -> Type
The tag
is used purely to distinguish between different states,
and is not required at run-time, as explicitly stated in the types of
get
and put
, which are used to access and update a state:
get : (0 tag : _) -> State tag t e => App {l} e t
put : (0 tag : _) -> State tag t e => (1 val : t) -> App {l} e ()
These use an auto
-implicit to pass around
a State
with the relevant tag
implicitly, so we refer
to states by tag alone. In helloCount
earlier, we used an empty type
Counter
as the tag:
data Counter : Type where -- complete definition
The list of errors e
is used to ensure that
states are only usable in the list of errors in which they are introduced.
States are introduced using new
:
new : t -> (1 p : State tag t e => App {l} e a) -> App {l} e a
Note that the type tells us new
runs the program with the state
exactly once.
Rather than using State
and Exception
directly, however,
we typically use interfaces to constrain the operations which are allowed
in a list of errors. Internally, State
is implemented via an
IORef
, primarily for performance reasons.