May 15, 2014 - CS240H
If you want to follow along on your laptop towards the end:
For ghc-7.6:
$ cabal install simple wai-handler-devel
For ghc-7.8
$ git clone git://github.com/alevy/simple.git
$ git clone git://github.com/alevy/postgresql-orm.git
$ cd simple
$ cabal install
$ cd ../postgresql-orm
$ cabal install
$ cabal install wai-handler-devel
You’ll also need to have PostgreSQL installed
Intro/motivation
Modeling a web application in Haskell
Build a content management system
Actually, that depends…
A busy space of frameworks
Used to be dominated by Java
terms like “Java Servlet Container”, “J2EE”, “Enterprise Java Beans”, “POJO”
everybody had a really bad experience with that in the late 90s/early 2Ks
Java is still the primary server-side language for, e.g. Google, Amazon
The cool kids are mostly using dynamic languages
Ruby/Ruby on Rails/Sinatra
Python/Django
node.js/express
PHP
etc…
e.g. no type declerations
x = 123
def incr(y)
y + 1
end
vs
protected static int x = 123;
public static int incr(int y) {
return y + 1;
}
like closures
Array.map(myarr, new Runnable() {
public void run(int elm) {
return elm * 42;
}
})
vs.
myarr.map {|elm| elm * 42}
Fast development and prototyping
Dynamic language GOOD because dynamic web sites!
“When rendering web pages, often you have very many components interacting on a web page. You have buttons over here and little widgets over there and there are dozens of them on a webpage, as well as possibly dozens or hundreds of web pages on your website that are all dynamic. […] using a statically typed language is actually quite inflexible. […] like the whole system has to type check just to be able to move a button around”
- Nick Kallen from Twitter
x = 123 -- :: Num a => a
incr y = y + 1 -- :: Num a => a -> a
map (* 42) myarr
Given the following two types:
data Request = Request {pathInfo :: [String], requestMethod :: Method, ...}
data Response = Response Status [Header] String
Fill in the type for an Application
:
type Application = ...
Boilerplate code: http://cs240h.scs.stanford.edu/Application.hs
```haskell
data Request = Request {pathInfo :: [String], requestMethod :: Method, ...}
data Response = Response Status [Header] String
type Application = Request -> IO Response
```
Common interface between servers and applications so you can mix-and-match
data Request = Request {
requestMethod :: Method
, httpVersion :: HttpVersion
, rawPathInfo :: ByteString
, rawQueryString :: ByteString
, requestHeaders :: RequestHeaders
, isSecure :: Bool
, remoteHost :: SockAddr
, pathInfo :: [Text]
, queryString :: Query
, requestBody :: Source IO ByteString
, vault :: Vault
, requestBodyLength :: RequestBodyLength
, requestHeaderHost :: Maybe B.ByteString
, requestHeaderRange :: Maybe B.ByteString
}
data Response
= ResponseFile Status ResponseHeaders FilePath (Maybe FilePart)
| ResponseBuilder Status ResponseHeaders Builder
| ResponseSource Status ResponseHeaders (forall b. WithSource IO (C.Flush Builder) b)
| ResponseRaw (forall b. WithRawApp b) Response
type Application = Request -> IO Response
Let’s build the simplest application that displays something in a browser
wai
and warp
:$ cabal install wai warp
module Main where
import qualified Data.ByteString.Lazy.Char8 as L8
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp (run)
app :: Application
app req = return $ responseLBS status200 [] $ L8.pack "Hello, World"
main :: IO ()
main = do
run 3000 app
(Very) quick intro to Simple
(Very) quick intro to postgresql-orm
Write some code
Simple is a web framework with one type:
newtype Controller s a = Controller {
runController :: s -> Request -> IO (Either Response a, s)
}
instance Monad Controller
instance Applicative Controller
instance MonadIO Controller
Very small wrapper around WAI’s Application
type
Let’s us refer to the Request
anywhere without passing it around
Let’s us refer to some application state anywhere without passing it around
Let’s us decide we’re ready to respond and stop computing
respond :: Response -> Controller s a
okHtml :: ByteString -> Response
notFound :: Response
respond $ okHtml "Hello world"
request :: Controller s Request
controllerState :: Controller s s
queryParam' :: Parseable p => Controller s p
parseForm :: Controller s ([Param], (ByteString, FileInfo ByteString))
-- Match on next dir in path
routeName :: Text -> Controller s () -> Controller s ()
routeName "articles" $ ...
-- Treat first dir in path as query param
routeVar :: Text -> Controller s () -> Controller s ()
routeName "articles" $ routeVar "name" $ ...
-- Match whole pattern of path
routePattern :: Text -> Controller s () -> Controller s ()
routePattern "/articles/:name" $ ...
-- Match if no path left
routeTop :: Controller s () -> Controller s ()
-- Match on request method
routeMethod :: Method -> Controller s () -> Controller s ()
routeMethod GET $ routePatter "/articles/:name"
-- Match hostname
routeHost :: ByteString -> Controller s () -> Controller s ()
Common case is to match on method and a particular path pattern:
get :: Text -> Controller s () -> Controller s ()
get ptrn ctrl = routeMethod GET $ routePattern ptrn ctrl
post :: Text -> Controller s () -> Controller s ()
post ptrn ctrl = routeMethod POST $ routePattern ptrn ctrl
So a typical small app might look like:
myapp :: Controller s ()
myapp = do
get "/" $ respond $ okHtml "Hello World"
get "/foo" $ respond $ okHtml "bar"
data Article = Article
{ articleId :: DBKey
, articleTitle :: Text
, articleBody :: Text
, articleShortName :: Text }
Model
class:class Model a where
modelInfo :: ModelInfo a
modelRead :: RowParser a
modelWrite :: a -> [Action]
data DBKey = DBKey !Int64 | NullKey
data ModelInfo a = ModelInfo {
modelTable :: ByteString
, modelColumns :: [ByteString]
, modelPrimaryColumn :: Int
, modelGetPrimaryKey :: a -> DBKey }
Model
derives Generic
we don’t need to write an implementation{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
data Article = Article
{ articleId :: DBKey
, articleTitle :: Text
, articleBody :: Text
, articleShortName :: Text } deriving (Show, Generic)
instance Model Article
save :: Model a => Connection -> a -> IO ()
findAll :: Model a => Connection -> IO [a]
findRow :: Model a => Connection -> DBRef a -> IO (Maybe a)
Maybe
)Either
)$ cabal install simple
$ smpl create my_cms