Haskell on a Horse ================== Haskell on a Horse (HoH) is a combinatorial web framework for the programming language Haskell. It is currently at an early, unsettled stage of development. It is available under the "BSD3" open-source license. Installing and Using HoH ------------------------ cabal install on-a-horse > {-#LANGUAGE Arrows, QuasiQuotes, ScopedTypeVariables #-} > import Web.Horse > import Control.Applicative > import Control.Arrow > import Control.Monad > import Control.Monad.Cont > import Data.Maybe > import Data.Monoid > import Data.List.Split (splitOn) > import Control.Arrow.Transformer.All > import Text.Pandoc Atomic Components ------------------- An HoH application is built up from atomic components. A component is a complete HoH application all by itself: it can render itself, and respond to input.
> > ex1 = proc url -> do > (fo,num::Maybe Integer) <- readForm "enter a number" -< () > returnA -< wrapForm fo > EXAMPLE
run this as a web app on port 8080 using ~~~~~~~~{.haskell} main = runHorse ex1 ~~~~~~~~ Side-by-Side Components -------------------------- Components can be rendered side-by-side within a page.
> ex2 :: HoH Url (Html ()) > ex2 = proc url -> do > (fo1, oper) <- enumForm "operation" > [("times", (*)), > ("plus", (+))] -< () > (fo2, x::Maybe Integer) <- readForm "x" -< () > (fo3, y::Maybe Integer) <- readForm "y" -< () > let result = show <$> (oper <*> x <*> y) > runHamlet -< [$hamlet| > %form!method=POST!action="" > Calculate a number! > %br > $fo1$ $fo2$ $fo3$ > Result: > $maybe result res > $res$ > %br > %input!type=submit |] EXAMPLE
Replacing one Component With Another --------------------------------------- Components can be replaced. A call to the arrow `throwAuto` will replace the nearest enclosing `catchAuto`. The new component will be called immediately, with no form input. ~~~~~~{.haskell} formSum label fs def = catchAuto $ proc _ -> do (fo,f) <- enumForm label fs -< () case f of Just f' -> throwAuto -< f' Nothing -> returnA -< setFormOut fo def ~~~~~~ Note: `def` is a default value to be used when no form is yet selected.
> ex3 :: HoH Url (Html ()) > ex3 = formSum "example to run" [("example 1",ex1),("example 2",ex2)] mempty > >>> arr wrapForm EXAMPLE
A More Complex Example ------------------------- By combining the techniques above, sophisticated pages can be made with little code.
> ex4 = proc url -> do > (fo,result) <- term "expression" -< () > runHamlet -< [$hamlet| > %form!method=POST!action="" > $fo$ > Result: > $maybe result res > $show res$ > %input!type=submit > %br |] > where > term :: String -> HoH () (FormOut, Maybe Integer) > term label = catchAuto $ formSum label > [("number", number label), > ("add",oper label "add" (+)), > ("multiply",oper label "multiply" (*))] (mempty, Nothing) > > number :: String -> WithError (HoH () (FormOut, Maybe Integer)) > () (FormOut, Maybe Integer) > number termLabel = proc () -> do > fo1 <- linkForm "cancel" (term termLabel) -< () > (fo2,x) <- readForm "number" -< () > returnA -< (fo1 `mappend` fo2, x) > > oper termLabel label f = proc () -> do > (fo1) <- linkForm "cancel" (term termLabel) -< () > (fo2,x) <- liftError (term "x") -< () > (fo3,y) <- liftError (term "y") -< () > out <- runHamlet -< [$hamlet| > %div.oper > $fo1$ > $label$ > %br > $fo2$ $fo3$ |] > returnA -< (out, f <$> x <*> y) EXAMPLE
Notes: * `throwAuto` works by adding an ErrorArrow to its argument. When it is called recursively, as in the example above, `liftError` may be required to avoid an infinite type. * `linkForm` acts much like `throwAuto`, except that it waits to throw its argument until the link it renders has been clicked. Building Atomic Components ------------------------- Atomic components should generally use the 'withInput' function. This will add two inputs to an arrow: the first is a unique label for the component, and the second is the current input to the arrow, or Nothing if there is no input. The label should be used as a name in any form input or query parameters. Here is the code for `linkForm`. ~~~~~~{.haskell} linkForm linkName f = withInput $ proc ((),nm,iname) -> do case iname of Just _ -> throwAuto -< f Nothing -> returnA -< (link linkName nm) ~~~~~~ (`link "name" "label"` produces `name`) Handling urls ------------- `runHorse` sends the URL as the sole argument to the handler. A function, `dispatch`, is available to construct multi-page applications.
> ex5 = proc url -> do > (dispatch $ staticUrls fourOhFour $ > [("", urls), > ("ex1", ex1), > ("ex2", ex2), > ("ex3", ex3), > ("ex4", ex4)]) -< (url,url) EXAMPLE
> fourOhFour = proc url -> do > runHamlet -< [$hamlet| Page not found |] > urls = proc url -> do > runHamlet -< [$hamlet| > %a!href=ex1 example 1 > %br > %a!href=ex2 example 2 > %br > %a!href=ex3 example 3 > %br > %a!href=ex4 example 4 > %br > %a!href=ex5 example 5 > %br |] > Running the Tutorial -------------------- This tutorial is a sort of self-executing markdown (pandoc) file. This is the code to run it. > main = do > tut <- readFile "tutorial.lhs" > tmpl <- getDefaultTemplate Nothing "html" > let pd = readMarkdown defaultParserState{ stateLiterateHaskell=True } tut > let tut' = writeHtmlString defaultWriterOptions{ > writerStandalone=True, > writerTemplate= either (error . show) id tmpl > } pd > let ts = map preEscapedString $ splitOn ("EXA"++"MPLE") tut' > runHorse $ proc url -> do > fo1 <- ex1 -< url > fo2 <- ex2 -< url > fo3 <- ex3 -< url > fo4 <- ex4 -< url > fo5 <- ex5 -< url > let vals = interleave ts [fo1,fo2,fo3,fo4,fo5] > runHamlet -< [$hamlet| > $forall vals val > $val$ |] > interleave (x:xs) (y:ys) = (x:y:interleave xs ys) > interleave [] ys = ys > interleave xs [] = xs > -- Jason Hart Priestley, July 26, 2010. (jason @ this domain)