Web Dev with Fn
Today I'll talk about...
- Thinking about apps as functions
- A super-simple text-only "website"
- Using WAI for more complicated responses
- Some nifty things about Fn's routing
Stuff you should know
before starting your app
- Type basics
- Making a data type an instance of a typeclass
- Maybe
- IO
What is a web app?
app :: Request
→ Response
- Takes a request
- Returns a response
app :: Request → Context
→ IO Response
- Takes a request and some context
- Does stuff!
- Returns a response
What is Fn?
Fn gives you tools for:
- Representing the application state
- Parsing a request and routing
- Constructing a response
- Interfacing with WAI and Warp
a simple website using Fn
fnhaskell.com/tutorial
Application State
data Ctxt = Ctxt FnRequest
instance RequestContext Ctxt where
getRequest (Ctxt req) = req
setRequest (Ctxt oldReq) newReq = Ctxt newReq
WAI
-
Web Application Interface
-
Common interface for frameworks and libraries
-
Lots of nice middleware for logging, sessions, etc
Warp
the actual server
Fn + WAI + Warp
import Web.Fn
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
main :: IO ()
main = run 3000 waiApp
waiApp :: Application
waiApp = toWAI (Ctxt defaultFnRequest) site
run :: Int → Application → IO ()
toWAI :: ctxt → (ctxt → IO Response) → Application
site :: ctxt → IO Response
Routing and Handler
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH ]
`fallthrough` notFoundText "Page not found."
indexH :: Ctxt -> IO (Maybe Response)
indexH ctxt = okText "Welcome to my first Haskell website."
The entire app
import Web.Fn
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
data Ctxt = Ctxt FnRequest
instance RequestContext Ctxt where
getRequest (Ctxt req) = req
setRequest (Ctxt oldReq) newReq = Ctxt newReq
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH ]
`fallthrough` notFoundText "Page not found."
indexH :: Ctxt -> IO (Maybe Response)
indexH ctxt = okText "Welcome to my first Haskell website."
main :: IO ()
main = run 3000 waiApp
waiApp :: Application
waiApp = toWAI (Ctxt defaultFnRequest) site
Responses
Responses
- okText & okHtml
- notFoundText & notFoundHtml
- errorText & errorHtml
- ... or construct your own with WAI's Response type
Custom Response
import Network.Wai (responseLBS)
import Network.HTTP.Types.Status (status418)
coffeeHandler :: Context -> IO (Maybe Response)
coffeeHandler ctxt =
return $ Just (responseLBS status418
[]
"I'm a teapot not a coffeemaker.")
Templating options
-
Lucid
-
Heist
-
Blaze
-
probably many more!!
Lucid
import Lucid
-- ...
indexView :: Html ()
indexView = do
html_ $ do
head_ $ do
title_ "My fancy Haskell site"
body_ $ do
p_ "Welcome to my FANCY Haskell website"
Lucid
lucidHtml :: Html () -> IO (Maybe Response)
lucidHtml h = okHtml $ toStrict $ renderText h
Html () -> Data.Text.Lazy
Data.Text.Lazy -> Data.Text.Strict
Data.Text.Strict -> IO (Maybe Response)
indexHandler :: Context -> IO (Maybe Response)
indexHandler ctxt = ????
indexHandler :: Context -> IO (Maybe Response)
indexHandler ctxt = lucidHtml indexView
Lucid
baseView :: Text -> Html () -> Html ()
baseView title rest =
html_ $ do
head_ $ do
link_ [ href_ "style.css",
rel_ "stylesheet",
type_ "text/css" ]
title_ (toHtml title)
body_ $ rest
indexView :: Html ()
indexView =
baseView "My fancy Haskell site" $
body_ $
p_ "Welcome to my FANCY Haskell website"
Deeper into Routes
route
routes :: Ctxt -> IO (Maybe Response)
routes ctxt =
route ctxt [ {- maybe matches ==> maybe handles -}
, {- maybe matches ==> maybe handles -}
, {- maybe matches ==> maybe handles -} ]
site :: Ctxt -> IO Response
site = routes ctxt `fallthrough` notFoundText "not found"
maybe a route matches, maybe a handler returns a response
if not: fallthrough to this other response
patterns
end path "blah" method "GET" anything
matches when there's nothing left
matches "blah"
matches only GET requests
matches anything!
patterns
segment param "blah"
passes a segment to the handler
passes the value of param "blah" to the the handler
Say hello!
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH
, path "hello" // end ==> helloH
, path "hello" // segment // end ==> helloNameH ]
`fallthrough` notFoundText "Page not found."
helloNameH :: Ctxt -> Text -> IO (Maybe Response)
helloNameH ctxt name = okText ("Hello, " <> name <> "!")
Maybe rudeness
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH
, path "hello" // segment // end ==> helloNameH
, path "hello" ==> rudeHelloH ]
`fallthrough` notFoundText "Page not found."
helloNameH :: Ctxt -> Text -> IO (Maybe Response)
helloNameH ctxt name =
if name == "dbp"
then return Nothing
else okText ("Hello, " <> name <> "!")
rudeHelloH :: Ctxt -> Text -> IO (Maybe Response)
rudeHelloH ctxt name = okText "Not you again."
Types!
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH
, path "add" // segment
// segment
// end ==> addNumbersH
, path "add" // segment
// segment
// end ==> addWordsH ]
`fallthrough` notFoundText "Page not found."
Same pattern, but two different handlers?
localhost:6000/add/(segment)/(segment)
Types!
addNumbersH :: Ctxt -> Int -> Int -> IO (Maybe Response)
addNumbersH ctxt number1 number2 =
let sum = number1 + number2 in
okText (show number1 <> " plus " <>
show number2 <> " is " <> sum <> ".")
addWordsH :: Ctxt -> Text -> Text -> IO (Maybe Response)
addWordsH ctxt word1 word2 =
okText (word1 <> " plus " <> word2 <>
" is " <> word1 <> word2 <> ".")
localhost:6000/add/1/2
1 plus 2 is 3
localhost:6000/add/ny/haskell
ny plus haskell is nyhaskell
Types!
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH
, path "add" // param "n1"
// param "n2"
// end ==> addNumbersH
, path "add" // segment
// segment
// end ==> addNumbersH ]
`fallthrough` notFoundText "Page not found."
Different patterns, but same handler
localhost:6000/add?n1=1&n2=2
localhost:6000/add/1/2
Types!
site :: Ctxt -> IO Response
site ctxt =
route ctxt [ end ==> indexH
, path "add" // segment // segment // end ==> addNumbersH
, path "add" // param "n1" // param "n2" ==> addNumbersH
, path "add" // segment // segment // end ==> addWordsH ]
`fallthrough` notFoundText "Page not found."
addNumbersH :: Ctxt -> Int -> Int -> IO (Maybe Response)
addNumbersH ctxt number1 number2 =
let sum = number1 + number2 in
okText (show number1 <> " plus " <>
show number2 <> " is " <> sum <> ".")
addWordsH :: Ctxt -> Text -> Text -> IO (Maybe Response)
addWordsH ctxt word1 word2 =
okText (word1 <> " plus " <> word2 <> " is " <> word1 <> word2 <> ".")
www.github.com/emhoracek/fntutorial
fnhaskell.com/tutorial
fnhaskell.com
Web Dev with Fn
By emhoracek
Web Dev with Fn
- 2,064