Web Dev in Haskell with Fn PART 1

About me

  • Software developer at Position Development in NYC
  • Lead developer of platform used by several independent magazines (written in Haskell)

In this workshop, we'll...

  • Make a simple website using Fn
  • Learn some Haskell along the way

git clone https://github.com/emhoracek/monadic-party-fn

cd monadic-party-fn

brew install postgresql || apt-get postgresql

stack setup

stack build

In this session...

  • Thinking about apps as functions
  • IO and Maybe types
  • Typeclasses
  • A super-simple text-only "website"

What is a web app?

app :: Request 

       → Response

  • Takes a request
  • Returns a response

Q: Is it possible to write a function like this?

Q: How would a web app like this be limited?

Pure functions

  • Put the same parameters in, get the same result out
  • No side effects

How does the type need to change

to deal with all this?

IO refresher

  • Q: What is the IO type?
  • Q: Why is it useful?
  • Q: How do you use it?

Using IO

sayHello = do
    name <- getLine
    putStrLn ("Hello " ++ name)

getLine :: IO Text

putStrLn :: IO ()

app :: RequestContext ctxt

        ⇒ ctxt → 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

Q: Why Fn and not

(Yesod || Servant || Spock)?

A: Because I like it

simple & easy to use

no Template Haskell

no language extensions required

the way the typed routes work is really cool :o

Application State

data Ctxt = Ctxt { req    :: FnRequest
                 , db     :: Pool Connection
                 , redis  :: Redis.Connection
                 , config :: Config 
                 ... etc ... }
myHandler :: Ctxt -> UserId -> IO (Maybe Response)
myHandler ctxt userId = do
    mUser <- getUserById (db ctxt) userId
    ...
getUserById :: Pool Connection -> UserId -> IO (Maybe User)

Application State

data Ctxt = Ctxt { req        :: FnRequest
                 , enviroment :: String
                 , sendMail   :: (String -> IO ())
                 ... etc ... }
data MyCtxt = MyCtxt { likesPizza    :: Bool
                     , request       :: FnRequest
                     , favoriteColor :: Text }

You can make whatever datatype you want, with whatever fields you want! Just make sure to include a field for Fn's `FnRequest`

data TinyCtxt = TinyCtxt FnRequest

src/FirstSite.hs

Fn needs to be able to

access the request.

 

If we can make our type whatever we want, how can Fn know how to get the request?

Typeclass time

data Character =
  Character { charaName :: String
            , charaClass :: Class }

data Class = Bard | Fighter | Druid | Paladin

character = Character "Hela" Fighter
> print character

<interactive>:10:1: error:
    • No instance for (Show Character) arising from a use of ‘print’
print :: Show a => a -> IO ()

src/Typeclasses.hs

Typeclass time

data Character =
  Character { charaName :: String
            , charaClass :: Class }
    deriving Show

data Class = Bard | Fighter | Druid | Paladin
    deriving Show

character = Character "Hela" Fighter
> character
Character {charaName = "Hela", charaClass = Fighter}

src/Typeclasses.hs

Typeclass time

data Character =
  Character { charaName :: String
            , charaClass :: Class }

instance Show Character where
  show (Character cName cClass) = 
    cName ++ " is the " ++ show cClass

data Class = Bard | Fighter | Druid | Paladin
  deriving Show

character = Character "Hela" Fighter
> character
Hela is the Fighter

src/Typeclasses.hs

Typeclass time

RequestContext

data Ctxt = Ctxt FnRequest

instance RequestContext Ctxt where
  getRequest (Ctxt req) = req
  setRequest (Ctxt oldReq) newReq = Ctxt newReq

Make your type an instance of RequestContext!

data Context = Context { likesPizza    :: Bool
                       , request       :: FnRequest
                       , favoriteColor :: Text }

instance RequestContext Context where
  getRequest context = request context
  setRequest context newReq = context { request = newReq }

src/FirstSite.hs

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 8000 waiApp

initCtxt :: Ctxt
initCtxt = Ctxt defaultFnRequest

waiApp :: Application
waiApp = toWAI initCtxt site
run :: Int → Application → IO ()
toWAI :: ctxt  (ctxt  IO Response)  Application
site :: ctxt  IO Response

src/FirstSite.hs

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

src/FirstSite.hs

An entire app

import Web.Fn
import Network.Wai (Application, Response)
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 8000 waiApp

initCtxt :: Ctxt
initCtxt = Ctxt defaultFnRequest

waiApp :: Application
waiApp = toWAI initCtxt site

src/FirstSite.hs

Reference:

fnhaskell.com

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


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

helloNameH handles "/hello/party"

"Hello, party!"

Say hello! with params


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


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

helloNameH handles "/hello?name=party"

Say hello! with both!


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


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

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

Maybe?

Maybe

data Maybe a = Just a | Nothing

Maybe String  = Just String | Nothing

  • Just "hello"
  • Nothing

Maybe Int  = Just Int | Nothing

  • Just 42
  • Nothing

Maybe (String -> Int)  = Just (String -> Int) | Nothing

  • Just length
  • Nothing

Maybe

listToMaybe :: [a] -> Maybe a

listToMaybe [1,2,3] = Just 1

listToMaybe [] = Nothing

*** Exception: Prelude.head: empty list

head :: [a] -> a

head [1,2,3] = 1

head [] = ???

Maybe rudeness


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

helloNameH :: Ctxt -> Text ->  IO (Maybe Response)
helloNameH ctxt name = 
  if name == "Libby"
  then return Nothing
  else okText ("Hello, " <> name <> "!")

rudeHelloH :: Ctxt -> Text -> IO (Maybe Response)
rudeHelloH ctxt name = okText "ugh, you again"

Types!


site :: Ctxt -> IO Response
site ctxt =
  route ctxt [ end               ==> indexH
             , path "add" // segment 
                          // segment 
                                 ==> addNumbersH

             , path "add" // segment 
                          // segment 
                                 ==> addWordsH ]
  `fallthrough` notFoundText "Page not found."

Same pattern, but two different handlers

localhost:8000/add/(segment)/(segment)

Types!


addNumbersH :: Ctxt -> Int -> Int -> IO (Maybe Response)
addNumbersH ctxt number1 number2 =
  let sum = number1 + number2 in
  okText (tshow number1 <> " plus " <>
          tshow number2 <> " is " <> tshow sum <> ".")



addWordsH :: Ctxt -> Text -> Text -> IO (Maybe Response)
addWordsH ctxt word1 word2 = 
  okText (word1 <> " plus " <> word2 <> 
          " is " <> word1 <> word2 <> ".")

localhost:8000/add/1/2

1 plus 2 is 3

localhost:8000/add/monadic/party

monadic plus party is monadic party

How does it work?


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

addNumbersH :: Ctxt -> Int -> Int -> IO (Maybe Response)
addNumbersH ctxt number1 number2 =
  let sum = number1 + number2 in
  okText (tshow number1 <> " plus " <>
          tshow number2 <> " is " <> tshow sum <> ".")

addWordsH :: Ctxt -> Text -> Text -> IO (Maybe Response)
addWordsH ctxt word1 word2 = 
  okText (word1 <> " plus " <> word2 <> " is " <> word1 <> word2 <> ".")

no arguments

one Text argument

two Int arguments

two Text arguments

this is a list of Routes that all seem very different -- how are they the same type?

fnhaskell.com

https://slides.com/emhoracek/web-dev-with-fn-21/

twitter: @horrorcheck

Web Dev in Haskell with Fn: Part 1

By emhoracek

Web Dev in Haskell with Fn: Part 1

  • 1,006