Safe Haskell | Safe |
---|---|
Language | Haskell2010 |
Bento manages stateful components. It is inspired by Stuart Sierra's Component library for Clojure.
- class Component component where
- type Dependencies component :: *
- start :: Dependencies component -> IO component
- stop :: component -> IO ()
Documentation
class Component component where Source
A stateful component. Usually you will define an instance of this class to a data type that contains all of its runtime state.
data Example = Example { handle ::Handle
} instanceComponent
Example where -- To be defined...
type Dependencies component :: * Source
Everything necessary to start the component. Typically this is a
tuple, but you are free to use whichever data structure you want. If
your component does not have any dependencies, use ()
, the empty
tuple.
typeDependencies
Example = (FilePath
,IOMode
)
start :: Dependencies component -> IO component Source
Starts the component. This is where you should do things like open
file handles, set up connections, and generally acquire resources. If
anything goes wrong, just throw an exception, preferrably with
throwIO
.
This function should not block forever. If you need to start something
that should keep running, like a server, put it on another thread with
forkIO
.
start
(path, mode) = do h <-openFile
path mode let component = Example { handle = h }return
component
Complete example
The follow is a complete example of using Component
s to build a larger
system, which is itself a Component
.
For this simple example, we will be starting a web server. The only piece of configuration we need is the port to listen on. We will get that from the environment. If it's not available, we'll fall back to a default.
data Config = Config { port :: Int } instanceComponent
Config where typeDependencies
Config = ()start
() = do p <-lookupEnv
"PORT" let config = Config { port =fromMaybe
8080 p }return
config
Now that we have the config, we can go ahead and set up the server. It
doesn't have to care how the config gets the port. We just have to list the
config as a dependency. We'll also need the Application
we
want to run on the server.
Since start
shouldn't block forever, we fire up the server on a separate
thread. We keep track of the thread ID so that we can shut down the server
when we stop
by killing the thread.
data Server = Server { threadId ::ThreadId
} instanceComponent
Server where typeDependencies
Server = (Config,Application
)start
(config, application) = do tid <-forkIO
(run
(port config) application) let server = Server { threadId = tid } return serverstop
server = dokillThread
(threadId server)
With the config and server in hand, we can combine them into a larger
system. The system will need the Application
we want to run,
but it will handle start
ing the config and passing it to the server.
To stop
the system, we stop
each Component
in the reverse order that
we start
ed them.
data System = System { config :: Config, server :: Server } instanceComponent
System where typeDependencies
System = (Application
)start
(application) = do c <-start
() s <-start
(c, application) let system = System { config = c, server = s }return
systemstop
system = dostop
(server system)stop
(config system)
To actually run the system, we start
it just like the other Component
s.
Once it's started, we want to wait forever until something sends us a
SIGINT
. Then we stop
the system.
main ::IO
() main = do let application _request respond = respond (responseLBS
ok200
[]empty
) system <-start
(application) sentinel <-newEmptyMVar
let handler _signal =putMVar
sentinel ()installHandler
sigINT
handlertakeMVar
sentinelstop
(system :: System)