| Copyright | (c) 2024 Sean Hess |
|---|---|
| License | BSD3 |
| Maintainer | Sean Hess <seanhess@gmail.com> |
| Stability | experimental |
| Portability | portable |
| Safe Haskell | Safe-Inferred |
| Language | GHC2021 |
Web.Hyperbole
Description
Create fully interactive HTML applications with type-safe serverside Haskell. Inspired by HTMX, Elm, and Phoenix LiveView
Synopsis
- liveApp :: (ByteString -> ByteString) -> Eff '[Hyperbole, Concurrent, IOE] Response -> Application
- run :: Port -> Application -> IO ()
- type Page es (views :: [Type]) = Eff (Reader (Root views) : es) (View (Root views) ())
- runPage :: (Hyperbole :> es, RunHandlers views es) => Page es views -> Eff es Response
- document :: View DocumentHead () -> ByteString -> ByteString
- quickStartDocument :: ByteString -> ByteString
- data DocumentHead
- quickStart :: View DocumentHead ()
- mobileFriendly :: View DocumentHead ()
- class Route a where
- routeRequest :: (Hyperbole :> es, Route route) => (route -> Eff es Response) -> Eff es Response
- routeUri :: Route a => a -> URI
- route :: Route a => a -> View c () -> View c ()
- data Hyperbole :: Effect
- request :: Hyperbole :> es => Eff es Request
- data Request = Request {}
- respondError :: Hyperbole :> es => ResponseError -> Eff es a
- respondErrorView :: Hyperbole :> es => Text -> View Body () -> Eff es a
- notFound :: Hyperbole :> es => Eff es a
- redirect :: Hyperbole :> es => URI -> Eff es a
- class ToQuery a where
- class FromQuery a where
- parseQuery :: QueryData -> Either String a
- query :: (FromQuery a, Hyperbole :> es) => Eff es a
- setQuery :: (ToQuery a, Hyperbole :> es) => a -> Eff es ()
- param :: (FromParam a, Hyperbole :> es) => Param -> Eff es a
- lookupParam :: (FromParam a, Hyperbole :> es) => Param -> Eff es (Maybe a)
- setParam :: (ToParam a, Hyperbole :> es) => Param -> a -> Eff es ()
- deleteParam :: Hyperbole :> es => Param -> Eff es ()
- queryParams :: Hyperbole :> es => Eff es QueryData
- class Session a where
- sessionKey :: Key
- cookiePath :: Maybe Path
- toCookie :: a -> CookieValue
- parseCookie :: CookieValue -> Either String a
- session :: (Session a, Default a, Hyperbole :> es) => Eff es a
- saveSession :: forall a es. (Session a, Hyperbole :> es) => a -> Eff es ()
- lookupSession :: forall a es. (Session a, Hyperbole :> es) => Eff es (Maybe a)
- modifySession :: (Session a, Default a, Hyperbole :> es) => (a -> a) -> Eff es a
- modifySession_ :: (Session a, Default a, Hyperbole :> es) => (a -> a) -> Eff es ()
- deleteSession :: forall a es. (Session a, Hyperbole :> es) => Eff es ()
- pageTitle :: Hyperbole :> es => Text -> Eff es ()
- trigger :: (HyperView id es, HyperViewHandled id view, Hyperbole :> es) => id -> Action id -> Eff (Reader view : es) ()
- pushEvent :: (ToJSON a, Hyperbole :> es) => Text -> a -> Eff es ()
- class (ViewId id, ViewAction (Action id)) => HyperView id es where
- class ViewId a
- class ViewAction a
- hyper :: forall id ctx. (HyperViewHandled id ctx, ViewId id) => id -> View id () -> View ctx ()
- class HasViewId m view where
- viewId :: m view
- button :: ViewAction (Action id) => Action id -> View id () -> View id ()
- search :: ViewAction (Action id) => (Text -> Action id) -> DelayMs -> View id ()
- dropdown :: ViewAction (Action id) => (opt -> Action id) -> opt -> View (Option opt id) () -> View id ()
- option :: (ViewAction (Action id), Eq opt, ToParam opt) => opt -> Text -> View (Option opt id) ()
- data Option opt id
- onClick :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a
- onDblClick :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a
- onMouseEnter :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a
- onMouseLeave :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a
- onInput :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => (Text -> Action id) -> DelayMs -> Attributes a -> Attributes a
- onLoad :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> DelayMs -> Attributes a -> Attributes a
- type DelayMs = Int
- onKeyDown :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Key -> Action id -> Attributes a -> Attributes a
- onKeyUp :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Key -> Action id -> Attributes a -> Attributes a
- data Key
- class FromForm (form :: Type) where
- class FromFormF (f :: (Type -> Type) -> Type) where
- formData :: forall form es. (FromForm form, Hyperbole :> es) => Eff es form
- class GenFields f (form :: (Type -> Type) -> Type) where
- genFields :: form f
- fieldNames :: forall form. GenFields FieldName form => form FieldName
- newtype FieldName a = FieldName Text
- data FormFields id
- type family Field (context :: Type -> Type) a
- data Identity a
- form :: ViewAction (Action id) => Action id -> View (FormFields id) () -> View id ()
- field :: forall (id :: Type) (a :: Type). FieldName a -> View (Input id a) () -> View (FormFields id) ()
- label :: View c () -> View c ()
- input :: InputType -> View (Input id a) ()
- checkbox :: Bool -> View (Input id a) ()
- radioGroup :: b -> View (Radio id a b) () -> View (Input id a) ()
- radio :: (Eq b, ToParam b) => b -> View (Radio id a b) ()
- select :: Eq opt => opt -> View (Option opt id) () -> View (Input id a) ()
- checked :: Attributable a => Bool -> Attributes a -> Attributes a
- textarea :: Maybe Text -> View (Input id a) ()
- submit :: View (FormFields id) () -> View (FormFields id) ()
- placeholder :: Attributable h => Text -> Attributes h -> Attributes h
- data InputType
- data Validated a
- = Invalid Text
- | NotInvalid
- | Valid
- isInvalid :: Validated a -> Bool
- validate :: Bool -> Text -> Validated a
- invalidText :: forall a id. Validated a -> View (Input id a) ()
- data QueryData
- class ToParam a where
- toParam :: a -> ParamValue
- class FromParam a where
- parseParam :: ParamValue -> Either String a
- decodeFormValue :: Maybe Text -> Either String a
- class ToEncoded a
- class FromEncoded a
- target :: forall id ctx. (HyperViewHandled id ctx, ViewId id) => id -> View id () -> View ctx ()
- data Response
- data Root (views :: [Type])
- newtype View c a = View {}
- type Name = Text
- newtype Table c a = Table (View c a)
- type AttValue = Text
- newtype TableColumns c dt a = TableColumns (Eff '[State [TableColumn c dt]] a)
- newtype TableHead id a = TableHead (View id a)
- data TableColumn c dt = TableColumn {}
- newtype ListItem c a = ListItem (View c a)
- text :: Text -> View c ()
- pre :: Text -> View c ()
- link :: URI -> View c () -> View c ()
- value :: Attributable h => Text -> Attributes h -> Attributes h
- style :: ByteString -> View c ()
- name :: Attributable h => Text -> Attributes h -> Attributes h
- none :: View c ()
- space :: View c ()
- col :: View c () -> View c ()
- row :: View c () -> View c ()
- (@) :: Attributable h => h -> (Attributes h -> Attributes h) -> h
- modAttributes :: Attributable h => (Map Name AttValue -> Map Name AttValue) -> h -> h
- att :: Attributable h => Name -> AttValue -> Attributes h -> Attributes h
- el :: View c () -> View c ()
- tag :: Text -> View c () -> View c ()
- raw :: Text -> View c ()
- renderLazyByteString :: View () () -> ByteString
- renderText :: View () () -> Text
- class_ :: Attributable h => AttValue -> Attributes h -> Attributes h
- addContext :: ctx -> View ctx () -> View c ()
- context :: forall c. View c c
- script' :: ByteString -> View c ()
- type_ :: Attributable h => Text -> Attributes h -> Attributes h
- modifyContext :: forall ctx0 ctx1. (ctx0 -> ctx1) -> View ctx1 () -> View ctx0 ()
- whenLoading :: Styleable h => (CSS h -> CSS h) -> CSS h -> CSS h
- cssEmbed :: ByteString
- scriptEmbed :: ByteString
- scriptEmbedSourceMap :: ByteString
- scriptLiveReload :: ByteString
- code :: Text -> View c ()
- src :: Attributable h => Text -> Attributes h -> Attributes h
- td :: View c () -> View c ()
- table :: [dt] -> TableColumns c dt () -> View c ()
- disabled :: Styleable h => CSS h -> CSS h
- loading :: Styleable h => CSS h -> CSS h
- content :: Attributable h => Text -> Attributes h -> Attributes h
- $sel:html:View :: View c a -> Eff '[Reader c] (Html a)
- img :: Text -> View c ()
- autofocus :: Attributable h => Attributes h -> Attributes h
- meta :: View c ()
- title :: Text -> View c ()
- httpEquiv :: Attributable h => Text -> Attributes h -> Attributes h
- charset :: Attributable h => Text -> Attributes h -> Attributes h
- script :: Text -> View c ()
- stylesheet :: Text -> View c ()
- nav :: View c () -> View c ()
- usersTable :: View c ()
- tcol :: forall dt c. TableHead c () -> (dt -> View c ()) -> TableColumns c dt ()
- th :: View c () -> TableHead c ()
- ol :: ListItem c () -> View c ()
- ul :: ListItem c () -> View c ()
- li :: View c () -> ListItem c ()
- module Web.Hyperbole.View.Embed
- class (e :: Effect) :> (es :: [Effect])
- data Eff (es :: [Effect]) a
- data URI = URI {}
- uri :: QuasiQuoter
- type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
- class Generic a where
- type family Rep a :: Type -> Type
- class Default a where
- def :: a
- class ToJSON a
- class FromJSON a
Introduction
Single Page Applications (SPAs) require the programmer to write two programs: a Javascript client and a Server, which both must conform to a common API
Hyperbole allows us to instead write a single Haskell program which runs exclusively on the server. All user interactions are sent to the server for processing, and a sub-section of the page is updated with the resulting HTML.
There are frameworks that support this in different ways, including HTMX, Phoenix LiveView, and others. Hyperbole has the following advantages
- 100% Haskell
- Type safe views, actions, routes, and forms
- Elegant interface with little boilerplate
- VirtualDOM updates over sockets, fallback to HTTP
- Easy to use
Like HTMX, Hyperbole extends the capability of UI elements, but it uses Haskell's type-system to prevent common errors and provide default functionality. Specifically, a page has multiple update targets called HyperViews. These are automatically targeted by any UI element that triggers an action inside them. The compiler makes sure that actions and targets match
Like Phoenix LiveView, it upgrades the page to a fast WebSocket connection and uses VirtualDOM for live updates
Like Elm, it uses an update function to process actions, but greatly simplifies the Elm Architecture by remaining stateless. Effects are handled by Effectful. forms are easy to use with minimal boilerplate
Hyperbole depends heavily on the following frameworks
Getting started
▶️ Intro
Hyperbole applications run via Warp and WAI
They are divided into top-level Pages, which run side effects (such as loading data from a database), then respond with an HTML View. The following application has a single Page that displays a static "Hello World"
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Web.Hyperbole
main :: IO ()
main = do
run 3000 $ do
liveApp quickStartDocument (runPage page)
page :: Page es '[]
page = do
pure $ el "Hello World"
HTML Views
Views are HTML fragments with a context
helloWorld ::Viewcontext () helloWorld =el"Hello World"
>>>renderText helloWorld<div>Hello World</div>
We can factor Views into reusable functions:
▶️ Simple
messageView :: Text ->Viewcontext () messageView msg =el$ text msg page' ::Pagees '[] page' = do pure $ messageView "Hello World"
Using atomic-css we can use functions to factor styles as well
▶️ CSS
import Web.Atomic.CSS header = bold h1 = header . fontSize 32 h2 = header . fontSize 24 page = gap 10 example = col page $ do el h1 "My Page"
Interactive HyperViews
▶️ Simple
We can embed one or more HyperViews to add type-safe interactivity to live subsections of our Page. To start, first define a data type (a ViewId) that uniquely identifies that subsection of the page:
data Message = Message
deriving (Generic, ViewId)
Make our ViewId an instance of HyperView:
- Create an
Actiontype with a constructor for every possible way that the user can interact with it - Write an
updatefor eachAction
instanceHyperViewMessage es where dataActionMessage = SetMessage Text deriving (Generic,ViewAction)update(SetMessage msg) = pure $ messageView msg
If an Action occurs, the contents of our HyperView will be replaced with the result of update.
To use our new HyperView, add the ViewId to the type-level list of Page, and then place it in the page view with hyper.
page ::Pagees '[Message] page = do pure $ doel"Unchanging Header"hyperMessage $ messageView "Hello World"
Now let's add a button to trigger the Action. Note that we must now update the View's context to match our ViewId. The compiler will tell us if we try to trigger actions that don't belong to our HyperView
messageView :: Text ->ViewMessage () messageView msg = doel~ bold $ text msgbutton(SetMessage "Goodbye") "Say Goodbye"
If the user clicks the button, the contents of hyper will be replaced with the result of update, leaving the rest of the page untouched.
View Functions
We showed above how we can factor Views into functions. It's best-practice to have a main View function for each HyperView. These take the form:
state -> View viewId ()
There's nothing special about state or View functions. They're just functions that take input data and return a view.
We can write multiple view functions with our HyperView as the context, and factor them however is most convenient:
messageButton :: Text ->ViewMessage () messageButton msg = dobutton(SetMessage msg) ~ border 1 $ text $ "Say " <> msg
Some View functions can be used in any context:
header :: Text ->Viewctx () header txt = doel~ bold $ text txt
With those two functions defined, we can refactor our main View to use them and avoid repeating ourselves
messageView :: Text -> View Message ()
messageView m = do
header m
messageButton "Salutations!"
messageButton "Good Morning!"
messageButton "Goodbye"
Managing State
HyperViews are stateless. They update based entirely on the Action. However, we can track simple state by passing it back and forth between the Action and the View
▶️ Counter
instanceHyperViewCounter es where dataActionCounter = Increment Int | Decrement Int deriving (Generic,ViewAction)update(Increment n) = do pure $ viewCount (n + 1)update(Decrement n) = do pure $ viewCount (n - 1) viewCount :: Int ->ViewCounter () viewCount n = row $ do col ~ gap 10 $ doel~ dataFeature $ text $ pack $ show n row ~ gap 10 $ dobutton(Decrement n) "Decrement" ~ Style.btnbutton(Increment n) "Increment" ~ Style.btn
Side Effects
For any real application with more complex state and data persistence, we need side effects.
Hyperbole relies on Effectful to compose side effects. We can use effects in a Page or an update. The Hyperbole effect gives us access to the request and client state, including sessions and the query params. In this example the page keeps the message in the query params
▶️ Query
page :: (Hyperbole:> es) =>Pagees '[Message] page = do prm <-lookupParam"msg" let msg = fromMaybe "hello" prm pure $ dohyperMessage $ messageView msg instanceHyperViewMessage es where dataActionMessage = Louder Text deriving (Generic,ViewAction)update(Louder msg) = do let new = msg <> "!"setParam"msg" new pure $ messageView new
To use an Effect other than Hyperbole, add it as a constraint to the Page and any HyperView instances that need it.
▶️ Effects
{-# LANGUAGE UndecidableInstances #-}
instance (Reader (TVar Int) :> es, Concurrent :> es) => HyperView Counter es where
data Action Counter
= Increment
| Decrement
deriving (Generic, ViewAction)
update Increment = do
n <- modify (+ 1)
pure $ viewCount n
update Decrement = do
n <- modify (subtract 1)
pure $ viewCount n
Then run the effect in your application
app :: TVar Int -> Application app var = doliveAppquickStartDocument(runReader var . runConcurrent $runPagepage)
See Effectful to read more about Effects
Databases and Custom Effects
A database is no different from any other Effect. It is recommended to create a custom effect to describe high-level data operations.
▶️ TodoMVC
data Todos :: Effect where
LoadAll :: Todos m [Todo]
Save :: Todo -> Todos m ()
Remove :: TodoId -> Todos m ()
Create :: Text -> Todos m TodoId
loadAll :: (Todos :> es) => Eff es [Todo]
loadAll = send LoadAll
Just like any effect, to use our custom Effect, we add it to any HyperView or Page as a constraint.
{-# LANGUAGE UndecidableInstances #-}
simplePage :: (Todos :> es) => Page es '[AllTodos, TodoView]
simplePage = do
todos <- Todos.loadAll
pure $ do
hyper AllTodos $ todosView FilterAll todos
We run a custom effect in our Application just like any other. The TodoMVC example implements the Todos Effect using Hyperbole sessions, but you could write a different runner that connects to a database instead.
main :: IO () main = dorun3000 $ doliveAppquickStartDocument(runTodosSession $runPagesimplePage)
Implementing a database runner for a custom Effect is beyond the scope of this documentation, but see the following:
- Effectful.Dynamic.Dispatch - Introduction to Effects
- NSO.Data.Datasets - Production Data Effect with a database runner
- Effectful.Rel8 - Effect for the Rel8 Postgres Library
Multiple HyperViews
We can add as many HyperViews to a page as we want. These can be muliple copies of the same HyperView with unique ViewId values, or completely different HyperViews.
Same HyperView, Unique ViewId
We can embed more than one of the same HyperView as long as the value of ViewId is unique. Let's update Message to allow for more than one value:
▶️ Simple
data Message = Message1 | Message2
deriving (Generic, ViewId)
Now we can embed multiple Message HyperViews into the same page. Each will update independently.
page ::Pagees '[Message] page = do pure $ dohyperMessage1 $ messageView "Hello"hyperMessage2 $ messageView "World!"
This is especially useful if we put identifying information in our ViewId, such as a database id. The viewId function can give us access to that info:
▶️ Load More
data Languages = Languages Offset deriving (Generic,ViewId) instanceHyperViewLanguages es where dataActionLanguages = Load deriving (Generic,ViewAction)updateLoad = do Languages offset <-viewIdls <- loadNextLanguages offset pure $ languagesView ls
Different HyperViews
Let's add both Count and Message HyperViews to the same page. Each will update independently:
page ::Pagees [Message, Count] page = do pure $ dohyperMessage $ messageView "Hello"hyperCount $ countView 0
Nesting HyperViews
We can nest smaller, more specific HyperViews inside of a larger parent. You might need this technique to display a list of items which might also need to update themselves individually
Let's imagine we want to display a list of Todos. The user can mark individual todos complete, and have them update independently. The more specific HyperView for each item might look like this:
▶️ TodoMVC
data TodoItem = TodoItem deriving (Generic,ViewId) instanceHyperViewTodoItem es where dataActionTodoItem = Complete Todo deriving (Generic,ViewAction)update(Complete todo) = do let new = todo{completed = True} pure $ todoView new
But we also want the entire list to refresh when a user adds a new todo. We need to create a parent HyperView for the whole list.
Add any nested HyperViews to Require to make sure they are handled. The compiler will let you know if you forget
data AllTodos = AllTodos deriving (Generic,ViewId) instanceHyperViewAllTodos es where type Require AllTodos = '[TodoItem] dataActionAllTodos = AddTodo Text [Todo] deriving (Generic,ViewAction)update(AddTodo txt todos) = do let new = Todo txt False : todos pure $ todosView new
Then we can embed the child HyperView into the parent View just like we do on a Page, by using hyper
todosView :: [Todo] ->ViewAllTodos () todosView todos = do forM_ todos $ todo -> dohyperTodoItem $ todoView todobutton(AddTodo "Shopping" todos) "Add Todo: Shopping"
Functions, not Components
You may be tempted to use HyperViews to create reusable "Components". This leads to object-oriented designs that don't compose well. We are using a functional language, so our main unit of reuse should be functions!
We showed earlier that we can write a View Function with a generic context that we can reuse in any view. A function like this might help us reuse styles or layout:
header :: Text ->Viewctx () header txt = doel~ bold $ text txt
But what if we want to reuse interactivity? We can pass an Action into the view function as a parameter:
styledButton :: (ViewAction(Actionid)) =>Actionid -> Text ->Viewid () styledButton clickAction lbl = dobuttonclickAction ~ btn $ text lbl where btn = pad 10 . bg Primary . hover (bg PrimaryLight) . rounded 5
We can create more complex view functions by passing state in as a parameter. Here's a button that toggles between a checked and unchecked state for any HyperView:
toggleCheckbox :: (ViewAction(Actionid)) => (Bool ->Actionid) -> Bool ->Viewid () toggleCheckbox setChecked isSelected = do tag "input" @ att "type" "checkbox" . onClick (setChecked (not isSelected)) . checked isSelected ~ big $ none where big = width 32 . height 32
View functions can be containers which wrap other Views:
progressBar :: Float ->Viewcontext () ->Viewcontext () progressBar pct contents = do row ~ bg Light $ do row ~ bg PrimaryLight . width (Pct pct) . pad 5 $ contents
Don't use HyperViews to keep your code DRY. Think about which subsections of a page ought to update independently. Those are HyperViews. If you need reusable interactivity, use view functions whenever possible. See the following example for a more complicated example.
Pages and Routes
An app will usually have multiple Pages with different Routes that each map to a unique url path:
data AppRoute
= Message -- /message
| Counter -- /counter
deriving (Generic, Eq, Route)
When we create our app, we can add a router function which maps a Route to a Page with routeRequest. The web page is completely reloaded each time you switch routes. Each Page is completely isolated.
main = dorun3000 $ doliveAppquickStartDocument(routeRequestrouter) where router Message =runPageMessage.page router Counter =runPageCounter.page
We can add type-safe links to other pages using route
menu ::Viewc () menu = dorouteMessage "Link to /message"routeCounter "Link to /counter"
If you need the same header or menu on all pages, use a view function:
exampleLayout ::Viewc () ->Viewc () exampleLayout contents = do col ~ grow $ doel~ border 1 $ "My Website Header" row $ do menu contents examplePage ::Pagees '[] examplePage = do pure $ exampleLayout $ doel"page contents"
As shown above, each Page can contain multiple interactive HyperViews to add interactivity
Examples
hyperbole.live is full of live examples demonstrating different features. Each example includes a link to the source code. Some highlights:
- ▶️ Simple
- ▶️ Counter
- ▶️ Concurrency
- ▶️ State
- ▶️ Requests
- ▶️ Data Lists
- ▶️ Forms
- ▶️ Interactivity
- ▶️ Error Handling
- ▶️ OAuth2
- ▶️ Javascript
- ▶️ Advanced
The National Solar Observatory uses Hyperbole to manage Level 2 Data pipelines for the DKIST telescope. It uses complex user interfaces, workers, databases, and more. The entire codebase is open source.
Application
liveApp :: (ByteString -> ByteString) -> Eff '[Hyperbole, Concurrent, IOE] Response -> Application Source #
Turn one or more Pages into a Wai Application. Respond using both HTTP and WebSockets
main :: IO ()
main = do
run 3000 $ do
liveApp quickStartDocument (runPage page)run :: Port -> Application -> IO () #
Run an Application on the given port.
This calls runSettings with defaultSettings.
Page
Document
document :: View DocumentHead () -> ByteString -> ByteString Source #
liveApp requires a function which turns an html fragment into an entire html document. Use this to import javascript, css, etc. Use quickStartDocument to get going quickly
app :: Application app = liveApp (document documentHead) (routeRequest router)
quickStartDocument :: ByteString -> ByteString Source #
A simple mobile-friendly document with all required embeds and live reload
liveAppquickStartDocument (routeRequestrouter)
data DocumentHead Source #
Create a custom <head> to use with document. Remember to include at least scriptEmbed!
import Web.Hyperbole (scriptEmbed, cssEmbed) documentHead :: View DocumentHead () documentHead = do title "My Website" script' scriptEmbed style cssEmbed script "custom.js" app :: Application app = liveApp (document documentHead) (routeRequest router)
quickStart :: View DocumentHead () Source #
A simple mobile-friendly header with all required embeds and live reload
mobileFriendly :: View DocumentHead () Source #
Set the viewport to handle mobile zoom
Type-Safe Routes
Derive this class to use a sum type as a route. Constructors and Selectors map intuitively to url patterns
data AppRoute
= Main
| Messages
| User UserId
deriving (Eq, Generic)
instance Route AppRoute where
baseRoute = Just Main
>>>routeUri Main/
>>>routeUri (User 9)/user/9
Minimal complete definition
Nothing
Methods
The route to use if attempting to match an empty path
matchRoute :: Path -> Maybe a Source #
Try to match a path to a route
routePath :: a -> Path Source #
Map a route to a path
routeRequest :: (Hyperbole :> es, Route route) => (route -> Eff es Response) -> Eff es Response Source #
Route URL patterns to different pages
import Example.Docs.Page.Messages qualified as Messages import Example.Docs.Page.Users qualified as Users type UserId = Int data AppRoute = Main | Messages | User UserId deriving (Eq, Generic) instanceRouteAppRoute where baseRoute = Just Main router :: (Hyperbole:> es) => AppRoute ->EffesResponserouter Messages =runPageMessages.page router (User cid) =runPage$ Users.page cid router Main = do pure $ view $ doel"click a link below to visit a page"routeMessages "Messages"route(User 1) "User 1"route(User 2) "User 2"
route :: Route a => a -> View c () -> View c () Source #
A hyperlink to another route
>>>route (User 100) id "View User"<a href="/user/100">View User</a>
Hyperbole Effect
data Hyperbole :: Effect Source #
The Hyperbole Effect allows you to access information in the Request, manually respond, and manipulate the Client session and query.
Instances
| type DispatchOf Hyperbole Source # | |
Defined in Web.Hyperbole.Effect.Hyperbole | |
Request
Constructors
| Request | |
Response
respondError :: Hyperbole :> es => ResponseError -> Eff es a Source #
Abort execution and respond with an error
respondErrorView :: Hyperbole :> es => Text -> View Body () -> Eff es a Source #
Abort execution and respond with an error view
notFound :: Hyperbole :> es => Eff es a Source #
Abort execution and respond with 404 Not Found
findUser :: (Hyperbole:> es, Users :> es) => Int ->Effes User findUser uid = do mu <- send (LoadUser uid) maybe notFound pure mu userPage :: (Hyperbole:> es, Users :> es) =>Pagees '[] userPage = do user <- findUser 100 -- skipped if user not found pure $ userView user
Query
▶️ Query
class ToQuery a where Source #
A page can store state in the browser query string. ToQuery and FromQuery control how a datatype is encoded to a full query string
data Filters = Filters
{ active :: Bool
, term :: Text
}
deriving (Generic, Eq, FromQuery, ToQuery)
>>>QueryData.render $ toQuery $ Filter True "asdf""active=true&search=asdf"
If the value of a field is the same as Default, it will be omitted from the query string
>>>QueryData.render $ toQuery $ Filter True """active=true"
>>>QueryData.render $ toQuery $ Filter False """"
Minimal complete definition
Nothing
Methods
class FromQuery a where Source #
Decode a type from a QueryData. Missing fields are set to def
data Filters = Filters
{ active :: Bool
, term :: Text
}
deriving (Generic, Eq, FromQuery, ToQuery)
>>>parseQuery $ QueryData.parse "active=true&search=asdf"Right (Filters True "asdf")
>>>parseQuery $ QueryData.parse "search=asdf"Right (Filters False "asdf")
Minimal complete definition
Nothing
Methods
parseQuery :: QueryData -> Either String a Source #
default parseQuery :: (Generic a, GFromQuery (Rep a)) => QueryData -> Either String a Source #
setQuery :: (ToQuery a, Hyperbole :> es) => a -> Eff es () Source #
Update the client's querystring to an encoded datatype. See ToQuery
instanceHyperViewTodos es where dataActionTodos = SetSearch Text deriving (Generic,ViewAction)update(SetSearch term) = do let filters = Filters term setQuery filters todos <- loadTodos filters pure $ todosView todos
lookupParam :: (FromParam a, Hyperbole :> es) => Param -> Eff es (Maybe a) Source #
Parse a single parameter from the query string if available
page :: (Hyperbole:> es) =>Pagees '[Message] page = do prm <-lookupParam"msg" let msg = fromMaybe "hello" prm pure $ dohyperMessage $ messageView msg
deleteParam :: Hyperbole :> es => Param -> Eff es () Source #
Delete a single parameter from the query string
Sessions
▶️ Sessions
class Session a where Source #
Configure a data type to persist in the session as a cookie. These are type-indexed, so only one of each can exist in the session
data Preferences = Preferences
{ color :: AppColor
}
deriving (Generic, ToEncoded, FromEncoded, Session)
instance Default Preferences where
def = Preferences White
Minimal complete definition
Nothing
Methods
sessionKey :: Key Source #
Unique key for this Session Type. Defaults to the datatypeName
default sessionKey :: (Generic a, GDatatypeName (Rep a)) => Key Source #
cookiePath :: Maybe Path Source #
By default Sessions are persisted only to the current page. Set to `Just "/"` to make an instance available application-wide
default cookiePath :: Maybe Path Source #
toCookie :: a -> CookieValue Source #
Encode type to a a cookie value
default toCookie :: ToEncoded a => a -> CookieValue Source #
parseCookie :: CookieValue -> Either String a Source #
Decode from a cookie value. Defaults to FromJSON
default parseCookie :: FromEncoded a => CookieValue -> Either String a Source #
Instances
| Session AuthFlow Source # | |
Defined in Web.Hyperbole.Effect.OAuth2 Methods sessionKey :: Key Source # cookiePath :: Maybe Path Source # toCookie :: AuthFlow -> CookieValue Source # parseCookie :: CookieValue -> Either String AuthFlow Source # | |
| Session Authenticated Source # | |
Defined in Web.Hyperbole.Effect.OAuth2 Methods sessionKey :: Key Source # cookiePath :: Maybe Path Source # toCookie :: Authenticated -> CookieValue Source # parseCookie :: CookieValue -> Either String Authenticated Source # | |
session :: (Session a, Default a, Hyperbole :> es) => Eff es a Source #
Load data from a browser cookie. If it doesn't exist, the Default instance is used
data Preferences = Preferences
{ color :: AppColor
}
deriving (Generic, ToEncoded, FromEncoded, Session)
instance Default Preferences where
def = Preferences White
page :: (Hyperbole :> es) => Page es '[Content]
page = do
prefs <- session @Preferences
pure $ el ~ bg prefs.color $ "Custom Background"
saveSession :: forall a es. (Session a, Hyperbole :> es) => a -> Eff es () Source #
Persist datatypes in browser cookies
data Preferences = Preferences
{ color :: AppColor
}
deriving (Generic, ToEncoded, FromEncoded, Session)
instance Default Preferences where
def = Preferences White
instance HyperView Content es where
data Action Content
= SetColor AppColor
deriving (Generic, ViewAction)
update (SetColor clr) = do
let prefs = Preferences clr
saveSession prefs
pure $ el ~ bg prefs.color $ "Custom Background"
lookupSession :: forall a es. (Session a, Hyperbole :> es) => Eff es (Maybe a) Source #
Return a session if it exists
deleteSession :: forall a es. (Session a, Hyperbole :> es) => Eff es () Source #
Remove a single Session from the browser cookies
Control Client
pageTitle :: Hyperbole :> es => Text -> Eff es () Source #
Set the document title
page :: (Hyperbole:> es) =>Pagees '[] page = do pageTitle "MyPageTitle" pure $el"Hello World"
trigger :: (HyperView id es, HyperViewHandled id view, Hyperbole :> es) => id -> Action id -> Eff (Reader view : es) () Source #
pushEvent :: (ToJSON a, Hyperbole :> es) => Text -> a -> Eff es () Source #
Dispatch a custom javascript event. This is emitted on the current hyper view and bubbles up to the document
▶️ Javascript
instanceHyperViewMessage es where dataActionMessage = AlertMe deriving (Generic,ViewAction)updateAlertMe = do pushEvent @Text "server-message" "hello" pure "Sent 'server-message' event"
function listenServerEvents() {
// you can listen on document instead, the event will bubble
Hyperbole.hyperView("Message").addEventListener("server-message", function(e) {
alert("Server Message: " + e.detail)
})
HyperView
class (ViewId id, ViewAction (Action id)) => HyperView id es where Source #
HyperViews are interactive subsections of a Page
Create an instance with a unique view id type and a sum type describing the actions the HyperView supports. The View Id can contain context (a database id, for example)
data Message = Message deriving (Generic,ViewId) instanceHyperViewMessage es where dataActionMessage = SetMessage Text deriving (Generic,ViewAction)update(SetMessage msg) = pure $ messageView msg
Associated Types
Outline all actions that are permitted in this HyperView
data Action Message = SetMessage Text | ClearMessage deriving (Generic, ViewAction)
type Require id :: [Type] Source #
Include any child hyperviews here. The compiler will make sure that the page knows how to handle them
type Require = '[ChildView]
type Require id = '[]
A unique identifier for a HyperView
data Message = Message1 | Message2
deriving (Generic, ViewId)
class ViewAction a Source #
Define every action possible for a given HyperView
instanceHyperViewMessage es where dataActionMessage = Louder Text deriving (Generic,ViewAction)update(Louder msg) = do let new = msg <> "!" pure $ messageView new
Instances
| ViewAction () Source # | |
| ViewAction (Action (Root views)) Source # | |
hyper :: forall id ctx. (HyperViewHandled id ctx, ViewId id) => id -> View id () -> View ctx () Source #
class HasViewId m view where Source #
Access the viewId in a View or update
data LazyData = LazyData TaskId deriving (Generic,ViewId) instance (Debug :> es, GenRandom :> es) =>HyperViewLazyData es where dataActionLazyData = Details deriving (Generic,ViewAction)updateDetails = do LazyData taskId <-viewIdtask <- pretendLoadTask taskId pure $ viewTaskDetails task
Interactive Elements
dropdown :: ViewAction (Action id) => (opt -> Action id) -> opt -> View (Option opt id) () -> View id () Source #
Type-safe dropdown. Sends (opt -> Action id) when selected. The default will be selected.
▶️ Filter
familyDropdown :: Filters -> View Languages ()
familyDropdown filters =
dropdown SetFamily filters.family ~ border 1 . pad 10 $ do
option Nothing "Any"
option (Just ObjectOriented) "Object Oriented"
option (Just Functional) "Functional"
option :: (ViewAction (Action id), Eq opt, ToParam opt) => opt -> Text -> View (Option opt id) () Source #
An option for a dropdown or select
Events
onClick :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a Source #
onDblClick :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a Source #
onMouseEnter :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a Source #
onMouseLeave :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> Attributes a -> Attributes a Source #
onInput :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => (Text -> Action id) -> DelayMs -> Attributes a -> Attributes a Source #
Run an action when the user types into an input or textarea.
WARNING: a short delay can result in poor performance. It is not recommended to set the value of the input
input (onInput OnSearch) 250 id
onLoad :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Action id -> DelayMs -> Attributes a -> Attributes a Source #
Send the action after N milliseconds. Can be used to implement lazy loading or polling. See Example.Page.Concurrent
viewTaskLoad ::ViewLazyData () viewTaskLoad = do -- 100ms after rendering, get the detailsel@ onLoad Details 100 ~ bg GrayLight . textAlign AlignCenter $ do text "..."
onKeyDown :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Key -> Action id -> Attributes a -> Attributes a Source #
onKeyUp :: (ViewAction (Action id), ViewContext a ~ id, Attributable a) => Key -> Action id -> Attributes a -> Attributes a Source #
Constructors
| ArrowDown | |
| ArrowUp | |
| ArrowLeft | |
| ArrowRight | |
| Enter | |
| Space | |
| Escape | |
| Alt | |
| CapsLock | |
| Control | |
| Fn | |
| Meta | |
| Shift | |
| OtherKey Text |
Type-Safe Forms
Painless forms with type-checked field names, and support for validation.
▶️ Forms
class FromForm (form :: Type) where Source #
Simple types that be decoded from form data
data ContactForm = ContactForm
{ name :: Text
, age :: Int
, isFavorite :: Bool
, planet :: Planet
, moon :: Moon
}
deriving (Generic, FromForm)
Minimal complete definition
Nothing
Methods
class FromFormF (f :: (Type -> Type) -> Type) where Source #
A Higher-Kinded type that can be parsed from a Form
data UserForm f = UserForm
{ user :: Field f User
, age :: Field f Int
, pass1 :: Field f Text
, pass2 :: Field f Text
}
deriving (Generic, FromFormF, GenFields Validated, GenFields FieldName)
Minimal complete definition
Nothing
formData :: forall form es. (FromForm form, Hyperbole :> es) => Eff es form Source #
Parse a full type from a submitted form body
class GenFields f (form :: (Type -> Type) -> Type) where Source #
Generate a Higher Kinded record with all selectors filled with default values. See GenField
data UserForm f = UserForm
{ user :: Field f User
, age :: Field f Int
, pass1 :: Field f Text
, pass2 :: Field f Text
}
deriving (Generic, FromFormF, GenFields Validated, GenFields FieldName)
newContactForm ::ViewNewContact () newContactForm = do row ~ pad 10 . gap 10 . border 1 $ do target Contacts $ do contactForm AddUser (genFields :: ContactForm Maybe) col $ do spacebuttonCloseForm ~ btnLight $ "Cancel"
Minimal complete definition
Nothing
fieldNames :: forall form. GenFields FieldName form => form FieldName Source #
Generate FieldNames for a form
▶️ Forms
data TodoForm f = TodoForm
{ task :: Field f Text
}
deriving (Generic, FromFormF, GenFields FieldName)
todoForm :: FilterTodo -> View AllTodos ()
todoForm filt = do
let f :: TodoForm FieldName = fieldNames
row ~ border 1 $ do
el ~ pad 8 $ do
button (ToggleAll filt) Icon.chevronDown ~ width 32 . hover (color Primary)
form SubmitTodo ~ grow $ do
field f.task $ do
input TextInput ~ pad 12 @ placeholder "What needs to be done?" . value ""Form FieldName. This is embeded as the name attribute, and refers to the key need to parse the form when submitted. See fieldNames
data FormFields id Source #
Context that allows form fields
type family Field (context :: Type -> Type) a Source #
Field allows a Higher Kinded Form to reuse the same selectors for form parsing, generating html forms, and validation
Field Identity Text ~ Text Field Maybe Text ~ Maybe Text
Identity functor and monad. (a non-strict monad)
Since: base-4.8.0.0
Instances
Form View
form :: ViewAction (Action id) => Action id -> View (FormFields id) () -> View id () Source #
Type-safe <form>. Calls (Action id) on submit
formView ::ViewAddContact () formView = do form Submit ~ gap 15 . pad 10 . flexCol $ doel~ Style.h1 $ "Add Contact" -- Make sure these names match the field names used by FormParse / formData field "name" $ do label $ do text "Contact Name" input Usernameplaceholder "contact name" ~ Style.input field "age" $ do label $ do text "Age" input Numberplaceholder "age" . value "0" ~ Style.input field "isFavorite" $ do label $ do row ~ gap 10 $ do checkbox False ~ width 32 text "Favorite?" col ~ gap 5 $ doel$ text "Planet" field "planet" $ do radioGroup Earth $ do radioOption Mercury radioOption Venus radioOption Earth radioOption Mars field "moon" $ do label $ do text "Moon" select Callisto ~ Style.input $ do option Titan "Titan" option Europa "Europa" option Callisto "Callisto" option Mimas "Mimas" submit "Submit" ~ btn where radioOption val = label ~ flexRow . gap 10 $ do radio val ~ width 32 text (pack (show val))
field :: forall (id :: Type) (a :: Type). FieldName a -> View (Input id a) () -> View (FormFields id) () Source #
checked :: Attributable a => Bool -> Attributes a -> Attributes a Source #
Set checkbox = checked via the client (VDOM doesn't work)
submit :: View (FormFields id) () -> View (FormFields id) () Source #
Button that submits the form
placeholder :: Attributable h => Text -> Attributes h -> Attributes h Source #
Choose one for inputs to give the browser autocomplete hints
Validation
Validation results for a Form. See validate
data UserForm f = UserForm
{ user :: Field f User
, age :: Field f Int
, pass1 :: Field f Text
, pass2 :: Field f Text
}
deriving (Generic, FromFormF, GenFields Validated, GenFields FieldName)
validateForm :: UserForm Identity -> UserForm Validated
validateForm u =
UserForm
{ user = validateUser u.user
, age = validateAge u.age
, pass1 = validatePass u.pass1 u.pass2
, pass2 = NotInvalid
}
validateAge :: Int -> Validated Int
validateAge a =
validate (a < 20) "User must be at least 20 years old"
Constructors
| Invalid Text | |
| NotInvalid | |
| Valid |
validate :: Bool -> Text -> Validated a Source #
specify a check for a Validation
validateAge :: Int -> Validated Int validateAge a = validate (a < 20) "User must be at least 20 years old"
Query Param Encoding
Key-value store for query params and sessions
class ToParam a where Source #
sessions, forms, and querys all encode data as query strings. ToParam and FromParam control how a datatype is encoded to a parameter.
-
This is equivalent to Web.HttpApiData, which is missing some instances and has some strange defaults
data AppColor = White | Red | Green deriving (Show, Generic,ToParam,FromParam)
Minimal complete definition
Nothing
Methods
toParam :: a -> ParamValue Source #
Instances
class FromParam a where Source #
Decode data from a query, session, or form parameter value
data AppColor = White | Red | Green deriving (Show, Generic,ToParam,FromParam)
Minimal complete definition
Nothing
Methods
parseParam :: ParamValue -> Either String a Source #
default parseParam :: (Generic a, GFromJSON Zero (Rep a)) => ParamValue -> Either String a Source #
Instances
Custom Encoding for embedding into web documents. Noteably used for ViewId and ViewAction
class FromEncoded a Source #
Custom Encoding for embedding into web documents. Noteably used for ViewId and ViewAction
Instances
| FromEncoded Encoded Source # | |
Defined in Web.Hyperbole.Data.Encoded | |
| FromEncoded AuthFlow Source # | |
Defined in Web.Hyperbole.Effect.OAuth2 | |
| FromEncoded Authenticated Source # | |
Defined in Web.Hyperbole.Effect.OAuth2 Methods parseEncoded :: Encoded -> Either String Authenticated Source # | |
| FromJSON a => FromEncoded (JSON a) Source # | |
Defined in Web.Hyperbole.Data.JSON | |
Advanced
target :: forall id ctx. (HyperViewHandled id ctx, ViewId id) => id -> View id () -> View ctx () Source #
Allow inputs to trigger actions for a different view
targetView ::ViewControls () targetView = do target Message $ dobutton(SetMessage "Targeted!") ~ btn $ "Target SetMessage"
A processed response for the client, which might be a ResponseError
data Root (views :: [Type]) Source #
The top-level view returned by a Page. It carries a type-level list of every HyperView used in our Page so the compiler can check our work and wire everything together.
Instances
| Generic (Action (Root views)) Source # | |
| Generic (Root views) Source # | |
| ViewAction (Action (Root views)) Source # | |
| ViewId (Root views) Source # | |
| HyperView (Root views) es Source # | |
| type Rep (Action (Root views)) Source # | |
| type Rep (Root views) Source # | |
| data Action (Root views) Source # | |
Defined in Web.Hyperbole.HyperView.Types | |
| type Require (Root views) Source # | |
Defined in Web.Hyperbole.HyperView.Types | |
Exports
View
Instances
| HasViewId (View ctx :: Type -> Type) (ctx :: Type) Source # | |
Defined in Web.Hyperbole.HyperView.ViewId | |
| Applicative (View ctx) Source # | |
| Functor (View c) Source # | |
| Monad (View ctx) Source # | |
| IsString (View c ()) Source # | |
Defined in Web.Hyperbole.View.Types Methods fromString :: String -> View c () # | |
| Attributable (View c a) Source # | |
Defined in Web.Hyperbole.View.Types | |
| Styleable (View c a) Source # | |
| Styleable (TableColumns c dt () -> View c ()) Source # | |
Defined in Web.Hyperbole.View.Tag Methods (~) :: (TableColumns c dt () -> View c ()) -> (CSS (TableColumns c dt () -> View c ()) -> CSS (TableColumns c dt () -> View c ())) -> TableColumns c dt () -> View c () # modCSS :: ([Rule] -> [Rule]) -> (TableColumns c dt () -> View c ()) -> TableColumns c dt () -> View c () # | |
newtype TableColumns c dt a Source #
Constructors
| TableColumns (Eff '[State [TableColumn c dt]] a) |
Instances
newtype TableHead id a Source #
Instances
| Applicative (TableHead id) Source # | |
Defined in Web.Hyperbole.View.Tag | |
| Functor (TableHead id) Source # | |
| Monad (TableHead id) Source # | |
| Styleable (TableHead id a) Source # | |
data TableColumn c dt Source #
Constructors
| TableColumn | |
value :: Attributable h => Text -> Attributes h -> Attributes h Source #
style :: ByteString -> View c () Source #
name :: Attributable h => Text -> Attributes h -> Attributes h Source #
(@) :: Attributable h => h -> (Attributes h -> Attributes h) -> h infixl 5 #
Apply an attribute to some html
el @ att "id" "main-content" $ do tag "img" @ att "src" "logo.png" tag "input" @ placeholder "message" ~ border 1
modAttributes :: Attributable h => (Map Name AttValue -> Map Name AttValue) -> h -> h #
att :: Attributable h => Name -> AttValue -> Attributes h -> Attributes h #
renderLazyByteString :: View () () -> ByteString Source #
renderText :: View () () -> Text Source #
class_ :: Attributable h => AttValue -> Attributes h -> Attributes h #
addContext :: ctx -> View ctx () -> View c () Source #
type_ :: Attributable h => Text -> Attributes h -> Attributes h Source #
modifyContext :: forall ctx0 ctx1. (ctx0 -> ctx1) -> View ctx1 () -> View ctx0 () Source #
whenLoading :: Styleable h => (CSS h -> CSS h) -> CSS h -> CSS h Source #
Apply CSS only when a request is in flight. See Example.Page.Contact
contactEditView :: User ->ViewContact () contactEditView u = doelcontactLoading ~ display None . whenLoading flexColel(contactEditViewSave u) ~ whenLoading (display None)
src :: Attributable h => Text -> Attributes h -> Attributes h Source #
table :: [dt] -> TableColumns c dt () -> View c () Source #
Create a type safe data table by specifying columns
data User = User {name :: Text, email :: Text}
usersTable :: [User] -> View c ()
usersTable us = do
table us $ do
tcol (th "Name" ~ hd) $ \u -> td ~ cell $ text u.name
tcol (th "Email" ~ hd) $ \u -> td ~ cell $ text u.email
where
hd = cell . bold
cell :: (Styleable h) => CSS h -> CSS h
cell = pad 4 . border 1content :: Attributable h => Text -> Attributes h -> Attributes h Source #
autofocus :: Attributable h => Attributes h -> Attributes h Source #
httpEquiv :: Attributable h => Text -> Attributes h -> Attributes h Source #
charset :: Attributable h => Text -> Attributes h -> Attributes h Source #
stylesheet :: Text -> View c () Source #
usersTable :: View c () Source #
ol :: ListItem c () -> View c () Source #
List elements do not include any inherent styling but are useful for accessibility. See list.
ol id $ do let nums = list Decimal li nums "one" li nums "two" li nums "three"
Embeds
Embedded CSS and Javascript to include in your document function. See quickStartDocument
module Web.Hyperbole.View.Embed
Effectful
class (e :: Effect) :> (es :: [Effect]) #
A constraint that requires that a particular effect e is a member of the
type-level list es. This is used to parameterize an Eff
computation over an arbitrary list of effects, so long as e is somewhere
in the list.
For example, a computation that only needs access to a mutable value of type
Integer would have the following type:
StateInteger:>es =>Effes ()
Instances
| (TypeError (('Text "There is no handler for '" ':<>: 'ShowType e) ':<>: 'Text "' in the context") :: Constraint) => e :> ('[] :: [Effect]) | |
Defined in Effectful.Internal.Effect Methods reifyIndex :: Int # | |
| e :> (e ': es) | |
Defined in Effectful.Internal.Effect Methods reifyIndex :: Int # | |
| e :> es => e :> (x ': es) | |
Defined in Effectful.Internal.Effect Methods reifyIndex :: Int # | |
The Eff monad provides the implementation of a computation that performs
an arbitrary set of effects. In , Eff es aes is a type-level list that
contains all the effects that the computation may perform. For example, a
computation that produces an Integer by consuming a String from the
global environment and acting upon a single mutable value of type Bool
would have the following type:
(ReaderString:>es,StateBool:>es) =>EffesInteger
Abstracting over the list of effects with (:>):
- Allows the computation to be used in functions that may perform other effects.
- Allows the effects to be handled in any order.
Instances
| IOE :> es => MonadBaseControl IO (Eff es) | Instance included for compatibility with existing code. Usage of Note: the unlifting strategy for |
| IOE :> es => MonadBase IO (Eff es) | Instance included for compatibility with existing code. Usage of |
Defined in Effectful.Internal.Monad | |
| HasViewId (Eff (Reader view ': es) :: Type -> Type) (view :: Type) Source # | |
| Fail :> es => MonadFail (Eff es) | |
Defined in Effectful.Internal.Monad | |
| MonadFix (Eff es) | |
Defined in Effectful.Internal.Monad | |
| IOE :> es => MonadIO (Eff es) | |
Defined in Effectful.Internal.Monad | |
| NonDet :> es => Alternative (Eff es) | Since: effectful-core-2.2.0.0 |
| Applicative (Eff es) | |
| Functor (Eff es) | |
| Monad (Eff es) | |
| NonDet :> es => MonadPlus (Eff es) | Since: effectful-core-2.2.0.0 |
| MonadCatch (Eff es) | |
Defined in Effectful.Internal.Monad | |
| MonadMask (Eff es) | |
Defined in Effectful.Internal.Monad Methods mask :: HasCallStack => ((forall a. Eff es a -> Eff es a) -> Eff es b) -> Eff es b # uninterruptibleMask :: HasCallStack => ((forall a. Eff es a -> Eff es a) -> Eff es b) -> Eff es b # generalBracket :: HasCallStack => Eff es a -> (a -> ExitCase b -> Eff es c) -> (a -> Eff es b) -> Eff es (b, c) # | |
| MonadThrow (Eff es) | |
Defined in Effectful.Internal.Monad Methods throwM :: (HasCallStack, Exception e) => e -> Eff es a # | |
| IOE :> es => MonadUnliftIO (Eff es) | Instance included for compatibility with existing code. Usage of Note: the unlifting strategy for |
Defined in Effectful.Internal.Monad | |
| Prim :> es => PrimMonad (Eff es) | |
| Monoid a => Monoid (Eff es a) | |
| Semigroup a => Semigroup (Eff es a) | |
| type PrimState (Eff es) | |
Defined in Effectful.Internal.Monad | |
| type StM (Eff es) a | |
Defined in Effectful.Internal.Monad | |
Other
Represents a general universal resource identifier using its component parts.
For example, for the URI
foo://anonymous@www.haskell.org:42/ghc?query#frag
the components are:
Constructors
| URI | |
Instances
| Data URI | |
Defined in Network.URI Methods gfoldl :: (forall d b. Data d => c (d -> b) -> d -> c b) -> (forall g. g -> c g) -> URI -> c URI # gunfold :: (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c URI # dataTypeOf :: URI -> DataType # dataCast1 :: Typeable t => (forall d. Data d => c (t d)) -> Maybe (c URI) # dataCast2 :: Typeable t => (forall d e. (Data d, Data e) => c (t d e)) -> Maybe (c URI) # gmapT :: (forall b. Data b => b -> b) -> URI -> URI # gmapQl :: (r -> r' -> r) -> r -> (forall d. Data d => d -> r') -> URI -> r # gmapQr :: forall r r'. (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> URI -> r # gmapQ :: (forall d. Data d => d -> u) -> URI -> [u] # gmapQi :: Int -> (forall d. Data d => d -> u) -> URI -> u # gmapM :: Monad m => (forall d. Data d => d -> m d) -> URI -> m URI # gmapMp :: MonadPlus m => (forall d. Data d => d -> m d) -> URI -> m URI # gmapMo :: MonadPlus m => (forall d. Data d => d -> m d) -> URI -> m URI # | |
| Generic URI | |
| Show URI | |
| NFData URI | |
Defined in Network.URI | |
| Eq URI | |
| Ord URI | |
| FromParam URI Source # | |
Defined in Web.Hyperbole.Data.Param Methods parseParam :: ParamValue -> Either String URI Source # | |
| ToParam URI Source # | |
Defined in Web.Hyperbole.Data.Param Methods toParam :: URI -> ParamValue Source # | |
| FromJSON URI | Since: aeson-2.2.0.0 |
Defined in Data.Aeson.Types.FromJSON | |
| FromJSONKey URI | Since: aeson-2.2.0.0 |
Defined in Data.Aeson.Types.FromJSON | |
| ToJSON URI | Since: aeson-2.2.0.0 |
| ToJSONKey URI | Since: aeson-2.2.0.0 |
Defined in Data.Aeson.Types.ToJSON | |
| Lift URI | |
| type Rep URI | |
Defined in Network.URI type Rep URI = D1 ('MetaData "URI" "Network.URI" "ntwrk-r-2.6.4.2-33a473b9" 'False) (C1 ('MetaCons "URI" 'PrefixI 'True) ((S1 ('MetaSel ('Just "uriScheme") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 String) :*: S1 ('MetaSel ('Just "uriAuthority") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (Maybe URIAuth))) :*: (S1 ('MetaSel ('Just "uriPath") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 String) :*: (S1 ('MetaSel ('Just "uriQuery") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 String) :*: S1 ('MetaSel ('Just "uriFragment") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 String))))) | |
uri :: QuasiQuoter #
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived #
The WAI application.
Note that, since WAI 3.0, this type is structured in continuation passing
style to allow for proper safe resource handling. This was handled in the
past via other means (e.g., ResourceT). As a demonstration:
app :: Application
app req respond = bracket_
(putStrLn "Allocating scarce resource")
(putStrLn "Cleaning up")
(respond $ responseLBS status200 [] "Hello World")
Representable types of kind *.
This class is derivable in GHC with the DeriveGeneric flag on.
A Generic instance must satisfy the following laws:
from.to≡idto.from≡id
Instances
type family Rep a :: Type -> Type #
Generic representation type
Instances
A class for types with a default value.
Minimal complete definition
Nothing
Instances
A type that can be converted to JSON.
Instances in general must specify toJSON and should (but don't need
to) specify toEncoding.
An example type and instance:
-- Allow ourselves to writeTextliterals. {-# LANGUAGE OverloadedStrings #-} data Coord = Coord { x :: Double, y :: Double } instanceToJSONCoord wheretoJSON(Coord x y) =object["x".=x, "y".=y]toEncoding(Coord x y) =pairs("x".=x<>"y".=y)
Instead of manually writing your ToJSON instance, there are two options
to do it automatically:
- Data.Aeson.TH provides Template Haskell functions which will derive an instance at compile time. The generated instance is optimized for your type so it will probably be more efficient than the following option.
- The compiler can provide a default generic implementation for
toJSON.
To use the second, simply add a deriving clause to your
datatype and declare a GenericToJSON instance. If you require nothing other than
defaultOptions, it is sufficient to write (and this is the only
alternative where the default toJSON implementation is sufficient):
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data Coord = Coord { x :: Double, y :: Double } deriving Generic
instance ToJSON Coord where
toEncoding = genericToEncoding defaultOptions
or more conveniently using the DerivingVia extension
deriving viaGenericallyCoord instanceToJSONCoord
If on the other hand you wish to customize the generic decoding, you have to implement both methods:
customOptions =defaultOptions{fieldLabelModifier=maptoUpper} instanceToJSONCoord wheretoJSON=genericToJSONcustomOptionstoEncoding=genericToEncodingcustomOptions
Previous versions of this library only had the toJSON method. Adding
toEncoding had two reasons:
toEncodingis more efficient for the common case that the output oftoJSONis directly serialized to aByteString. Further, expressing either method in terms of the other would be non-optimal.- The choice of defaults allows a smooth transition for existing users:
Existing instances that do not define
toEncodingstill compile and have the correct semantics. This is ensured by making the default implementation oftoEncodingusetoJSON. This produces correct results, but since it performs an intermediate conversion to aValue, it will be less efficient than directly emitting anEncoding. (this also means that specifying nothing more thaninstance ToJSON Coordwould be sufficient as a generically decoding instance, but there probably exists no good reason to not specifytoEncodingin new instances.)
Instances
A type that can be converted from JSON, with the possibility of failure.
In many cases, you can get the compiler to generate parsing code for you (see below). To begin, let's cover writing an instance by hand.
There are various reasons a conversion could fail. For example, an
Object could be missing a required key, an Array could be of
the wrong size, or a value could be of an incompatible type.
The basic ways to signal a failed conversion are as follows:
failyields a custom error message: it is the recommended way of reporting a failure;empty(ormzero) is uninformative: use it when the error is meant to be caught by some(;<|>)typeMismatchcan be used to report a failure when the encountered value is not of the expected JSON type;unexpectedis an appropriate alternative when more than one type may be expected, or to keep the expected type implicit.
prependFailure (or modifyFailure) add more information to a parser's
error messages.
An example type and instance using typeMismatch and prependFailure:
-- Allow ourselves to writeTextliterals. {-# LANGUAGE OverloadedStrings #-} data Coord = Coord { x :: Double, y :: Double } instanceFromJSONCoord whereparseJSON(Objectv) = Coord<$>v.:"x"<*>v.:"y" -- We do not expect a non-Objectvalue here. -- We could useemptyto fail, buttypeMismatch-- gives a much more informative error message.parseJSONinvalid =prependFailure"parsing Coord failed, " (typeMismatch"Object" invalid)
For this common case of only being concerned with a single
type of JSON value, the functions withObject, withScientific, etc.
are provided. Their use is to be preferred when possible, since
they are more terse. Using withObject, we can rewrite the above instance
(assuming the same language extension and data type) as:
instanceFromJSONCoord whereparseJSON=withObject"Coord" $ \v -> Coord<$>v.:"x"<*>v.:"y"
Instead of manually writing your FromJSON instance, there are two options
to do it automatically:
- Data.Aeson.TH provides Template Haskell functions which will derive an instance at compile time. The generated instance is optimized for your type so it will probably be more efficient than the following option.
- The compiler can provide a default generic implementation for
parseJSON.
To use the second, simply add a deriving clause to your
datatype and declare a GenericFromJSON instance for your datatype without giving
a definition for parseJSON.
For example, the previous example can be simplified to just:
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data Coord = Coord { x :: Double, y :: Double } deriving Generic
instance FromJSON Coord
or using the DerivingVia extension
deriving viaGenericallyCoord instanceFromJSONCoord
The default implementation will be equivalent to
parseJSON = ; if you need different
options, you can customize the generic decoding by defining:genericParseJSON defaultOptions
customOptions =defaultOptions{fieldLabelModifier=maptoUpper} instanceFromJSONCoord whereparseJSON=genericParseJSONcustomOptions