Web Dev in Haskell with Fn

Today I'll talk about...

  • Thinking about apps as functions
  • A super-simple text-only "website"
  • 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 :: RequestContext

            → IO Response

  • Takes a request in 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

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" // segment // end ==> helloNameH ]
       `fallthrough` notFoundText "Page not found."


helloNameH :: Ctxt -> Text ->  IO (Maybe Response)
helloNameH ctxt name = okText ("Hello, " <> name <> "!")

helloNameH handles "/hello/workjelly"

"Hello, workjelly!"

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."

Say hello! with params


site :: Ctxt -> IO Response
site ctxt = 
  route ctxt [ end ==> indexH
             , path "hello" // param "name" //
                 end ==> helloNameH ]
       `fallthrough` notFoundText "Page not found."


helloNameH :: Ctxt -> Text ->  IO (Maybe Response)
helloNameH ctxt name = okText ("Hello, " <> name <> "!")

helloNameH handles "/hello?name=workjelly"

Say hello! with both!


site :: Ctxt -> IO Response
site ctxt = 
  route ctxt [ end ==> indexH
             , path "hello" // segment // end ==> helloNameH
             , path "hello" // param "name" //
                 end ==> helloNameH ]
       `fallthrough` notFoundText "Page not found."


helloNameH :: Ctxt -> Text ->  IO (Maybe Response)
helloNameH ctxt name = okText ("Hello, " <> name <> "!")

helloNameH handles "/hello/workjelly" and "/hello?name=workjelly"

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/work/jelly

work plus jelly is workjelly

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."

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

Made with Slides.com