Introducing App#
App is declared as below, in a module Control.App, which is part of
the base libraries.
It is parameterised by an implicit
Path (which states whether the program’s execution path
is linear or might throw
exceptions), which has a default value that the program
might throw, and a List Error
(which gives a list of exception types which can be thrown, Error is
a synonym for Type):
data App : {default MayThrow l : Path} ->
(es : List Error) -> Type -> Type
It serves the same purpose as IO, but supports throwing and catching
exceptions, and allows us to define more constrained interfaces parameterised
by the list of errors es.
e.g. a program which supports console IO:
hello : Console es => App es ()
hello = putStrLn "Hello, App world!"
We can use this in a complete program as follows:
module Main
import Control.App
import Control.App.Console
hello : Console es => App es ()
hello = putStrLn "Hello, App world!"
main : IO ()
main = run hello
Or, a program which supports console IO and carries an Int state,
labelled Counter:
data Counter : Type where
helloCount : (Console es, State Counter Int es) => App es ()
helloCount = do c <- get Counter
put Counter (c + 1)
putStrLn "Hello, counting world"
c <- get Counter
putStrLn ("Counter " ++ show c)
To run this as part of a complete program, we need to initialise the state.
main : IO ()
main = run (new 93 helloCount)
For convenience, we can list multiple interfaces in one go, using a function
Has, defined in Control.App, to compute the interface constraints:
helloCount : Has [Console, State Counter Int] es => App es ()
0 Has : List (a -> Type) -> a -> Type
Has [] es = ()
Has (e :: es') es = (e es, Has es' es)
The purpose of Path is to state whether a program can throw
exceptions, so that we can know where it is safe to reference linear
resources. It is declared as follows:
data Path = MayThrow | NoThrow
The type of App states that MayThrow is the default.
We expect this to be the most
common case. After all, realistically, most operations have possible failure
modes, especially those which interact with the outside world.
The 0 on the declaration of Has indicates that it can only
be run in an erased context, so it will never be run at run-time.
To run an App inside IO, we use an initial
list of errors Init (recall that an Error is a
synonym for Type):
Init : List Error
Init = [AppHasIO]
run : App {l} Init a -> IO a
Generalising the Path parameter with l
means that we can invoke run for any application, whether the Path
is NoThrow or MayThrow. But, in practice, all applications
given to run will not throw at the top level, because the only
exception type available is the type AppHasIO, which is an empty data
type (it has no values). Any exceptions will have been introduced and handled
inside the App.