Hilt provides a set of opinionated batteries-included services for Haskell, and a way to use them together easily, allowing you to get the handle of Haskell.
It is intended to be used at the base level of your Haskell application, providing some structure for your business logic.
A basic example using the Logger
and Channel
services together. It simply writes any messages written to the channel, which the worker logs.
main = Hilt.manage $ do
logger <- Logger.load
chan <- Channel.load
Hilt.program $ do
Logger.debug logger "Starting up!"
Channel.worker chan (\text -> Logger.debug logger ("Received message: " <> text))
Channel.write chan "Hello world!"
Channel.write chan "Goodbye world!"
Hilt handles the underlying mechanics, threads, async behaviour, safety and service management/cleanup for us.
For a full, runnable example, see app/Main.hs.
With the Haskell tool stack
installed;
- Create a new project with
stack new <projectname> new-template
, or adjust themain
of an existing one - In your
project.cabal
under theexecutable
section- Add
hilt
to thebuild-depends
list - Add
default-extensions: OverloadedStrings
- Add
- In your
stack.yml
either add or merge the following settings:Stack doesn't support aextra-deps: - git: https://github.com/supermario/hilt.git commit: 88faaa1d0549fda9a309411ed1c19b6714a4ae8f # Current Master Sha
master
target, so you'll need to pin the latest SHA until Hilt is released.
Hilt currently provides the following types of services:
Logger
: basic Debug, Info, Warning and Error level logging serviceWebsocket
: websocket servicePostgres
: postgres connection pool and querying serviceChannel
: typed read/write channel service with workersCache
: an in-memory key-value cache service
You can use these services as-is, or as reference code to pull out and create your own services as needed – each one is contained in a single file. They are intended to be compact and easy to understand.
Hilt also provides some helpers; Config
, JSON
and Server
.
An simple STDOUT logger.
Create a handle with logger <- Logger.load
and then:
Usage | Description | |
---|---|---|
log | Logger.debug logger "debug message" |
Writes [Debug] debug message to STDOUT |
In addition to .debug
You can also use .info
, .warning
and .error
.
This service has a couple of moving parts. We need to
- specify what we want to do for
onJoined
andonReceive
events - boot the HTTP server.
Here's a simple example that just logs joins and receives and sends no messages:
main = Hilt.manage $ do
logger <- Logger.load
let
onJoined clientId clientCount = do
Logger.debug logger $ showt clientId <> " joined, " <> showt clientCount <> " connected."
return Nothing
onReceive clientId text =
Logger.debug logger $ showt clientId <> " said " <> showt text
websocket <- Websocket.load onJoined onReceive
Hilt.program $ do
Hilt.Server.runWebsocket websocket
Your program logic can now use the websocket
handle to:
Usage | Description | |
---|---|---|
send | Websocket.send websocket clientId "Hello world!" |
Send a message to a single client |
broadcast | Websocket.broadcast websocket "Hello world!" |
Send a message to all clients |
For a full, runnable example, see app/Main.hs.
A database service that handles connection pooling and configuration.
It expects a DATABASE_URL
ENV var with a postgresql URL to be present, i.e. DATABASE_URL=postgres://user:pass@hostname:5432/databasename
.
Create a handle with db <- Postgres.load
and then:
Usage | |
---|---|
query_ | Run a postgresql-simple query and decode the results to the specified type. events :: [Event] <- Postgres.query_ db "SELECT * FROM events" |
query | Run a paramaterised postgresql-simple query and decode the results to the specified type. Params interpolate into ? within the query. Use a singleton array [a] for a single param and tuples (a,b,...) for multiple params events :: [Event] <- Postgres.query db "SELECT * FROM ?" ["events"] |
execute | Run a postgresql-simple query that returns no results. Params interpolate into ? within the query. Use a singleton array [a] for a single param and tuples (a,b,...) for multiple params or unit () for none Postgres.execute db "DROP TABLE events" () |
listen | Forks a worker that listens to a LISTEN query and runs the given handler for each message Postgres.listen db "LISTEN an_event" (\text -> print text) |
queryP | Runs a persistent query. See the persistent guide. events :: [Event] <- Postgres.exec db $ selectList [] [] |
Also connection pooling currently only works for the queryP
function.
In future this might tend more towards something like the queryMaybe
interface described.
An in-memory key value cache. Contents are not persisted across app restarts.
Create a handle with cache <- Cache.load
and then:
Usage | Description | |
---|---|---|
insert | Cache.insert cache "mykey" "myvalue" |
Inserts a new key/value into the cache |
lookup | value <- Cache.lookup cache "mykey" |
Lookup a value by key. Returns a Maybe, as the item may not be found |
delete | Cache.delete cache "mykey" |
Deletes a given value, if the key exists |
keys | keys <- Cache.keys cache |
Retrieves a list of all keys from the cache |
size | size <- Cache.size cache |
Retrieves the size of the cache as an Int |
Cache currently requires both key and value to be type Text
. It will be extended to support any value type in future.
A channel is a simple text based queue. You can write values to it, and read values from it. Once a value is read, it is no longer on the queue.
You might use a channel to pass messages between different parts of your app, or trigger actions in a seperate thread. See app/Main.hs for an example.
Create a handle with chan <- Channel.load
and then:
Usage | Description | |
---|---|---|
write | Channel.write chan "Hello world!" |
Writes a text value to the channel |
read | text <- Channel.read chan |
Waits to read a single text value from the channel |
worker | Channel.worker chan (\text -> Logger.debug logger ("Received message: " <> text)) |
Fork a worker thread that runs given function for each read value |
Generally you should only have one worker per channel, as messages can only be read once.
This file has helpers for booting HTTP servers. They all read the following ENV vars:
var name | Default | Value |
---|---|---|
PORT | 8081 | 0-65535 |
ENV | "Development" | Hilt.Config.Environment values: Development | Test | Staging | Production |
- You can override by prefixing like so:
ENV=Staging PORT=3030 stack exec yourAppName
. Development
/Staging
environments use the development logger.Test
usesid
(no logging).- Default middlewares are here.
In future there will likely be a Hilt.Http
service, in the meantime any Wai
app will work. I'd recommend servant.
Usage | Description | |
---|---|---|
runHttp | runHttp waiApp defaultMiddlewares |
Run the given Wai app |
runWebsocket | runWebsocket socketHandle defaultMiddlewares |
Run the Websocket service at /ws |
runWebsocketAndHttp | runWebsocketAndHttp socketHandle waiApp defaultMiddlewares |
Run both the Websocket service and the Wai service together |
A number of common middlewares are provided, see Server.hs.
Hilt services are no more than an IO
value wrapped by Control.Monad.Managed
.
For example, say you wanted to write a Hilt service for a storage-backed channel that survives program restarts, you might:
- create your own channel service from scratch with baked in storage functionality
- or, create a
Hilt.Channel.DB
service and have it require aHilt.Handles.Postgres
service to use for persistence - or, use an existing implementation you have and run it under managed (for which Hilt.managed is just a wrapper), exposing a service interface
Services are very simple, take a look at Cache for example, which just wraps the Data.Cache
lib as-is.
Hilt is an implementation of the service pattern.
It intentionally avoids typeclass and monad-transformer approaches (ala 'Scrap your type classes') to experiment with a more explicit value-level approach.
It is intended to be used at the base level of your Haskell application, providing some structure for your IO-generating business logic.
Thread management is rather carefree presently and will be improved in future.
Logo by birdie brain from the Noun Project.