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,129