1351 lines
25 KiB
Markdown
1351 lines
25 KiB
Markdown
---
|
|
title: Web Programming and Streaming Data in Haskell
|
|
---
|
|
|
|
# Web Programming and Streaming Data in Haskell
|
|
|
|
* Michael Snoyman
|
|
* LambdaConf 2017
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
* How to get things done
|
|
* First hit Conduit, then hit Yesod
|
|
* Identify why you'd use these libraries
|
|
* Get you comfortable enough to use them
|
|
* More information after this talk:
|
|
* https://haskell-lang.org/library/conduit
|
|
* http://www.yesodweb.com/book
|
|
* Please ask questions!
|
|
|
|
---
|
|
|
|
## Prepare your machine
|
|
|
|
```
|
|
$ stack --resolver lts-8.12 --install-ghc
|
|
build classy-prelude-yesod
|
|
```
|
|
|
|
* Your hands should be warm pretty soon
|
|
* Make sure you're plugged in or have a great battery
|
|
|
|
---
|
|
|
|
## What is streaming data?
|
|
|
|
* Process a sequence of values of the same type
|
|
* Produce a sequence of values of the same type
|
|
* Don't keep all the data in memory at once
|
|
* Perform some actions in-between
|
|
* Probably more common than you'd think
|
|
|
|
---
|
|
|
|
## Alternatives
|
|
|
|
* Lazy lists: don't allow interleaved effects
|
|
* Lazy I/O: effects, exceptions pop up in unexpected places (evil!)
|
|
* Pipes: relies on higher layers (like pipes-parse) for things built-in with Conduit
|
|
* Streaming: makes some cases (like substreams) easier, other cases (multi-consumption) more difficult
|
|
|
|
----
|
|
|
|
## Goal in this talk:
|
|
|
|
* Talk you out of using lazy I/O
|
|
* Explain when lazy lists aren't enough
|
|
* Feel free to explore other streaming libraries, but today is about Conduit
|
|
|
|
---
|
|
|
|
## Common Examples
|
|
|
|
* Read data from/write data to a file
|
|
* Communicate over a socket
|
|
* Read data from a database
|
|
* Traverse a deep directory structure
|
|
* Implement a job queue
|
|
* Generate large HTTP response bodies
|
|
* Parsing
|
|
|
|
----
|
|
|
|
## Common Non-Examples
|
|
|
|
* Highly optimized CPU pipeline
|
|
* Operations requiring no interleaved effects
|
|
* World peace
|
|
|
|
---
|
|
|
|
## Hello World: Fold
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
main =
|
|
print $ runConduitPure
|
|
$ yieldMany [1..10]
|
|
.| sumC
|
|
```
|
|
|
|
* Pure operation
|
|
* Correct: this is a bad use case for Conduit :)
|
|
|
|
----
|
|
|
|
## File Copy
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
main = do
|
|
-- Create a source file
|
|
writeFile "input.txt" "This is a test."
|
|
|
|
runConduitRes $ sourceFile "input.txt"
|
|
.| sinkFile "output.txt"
|
|
```
|
|
|
|
* Copies a file
|
|
* Exception safety built in (magic of `Res`)
|
|
* Common Conduit terms: source and sink
|
|
|
|
----
|
|
|
|
## Data Transform
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
main =
|
|
print $ runConduitPure
|
|
$ yieldMany [1..10]
|
|
.| mapC (+ 1)
|
|
.| sinkList
|
|
```
|
|
|
|
* Again: you don't need Conduit for this
|
|
* Conduit most useful for pipelines
|
|
|
|
---
|
|
|
|
## Terminology
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
```
|
|
|
|
* `foo .| bar .| baz` is a *pipeline*
|
|
* `foo`, `bar`, and `baz` are *components* of the pipeline
|
|
* `foo` is *upstream* from `bar`, `baz` is *downstream* from `bar`
|
|
* `foo` can *yield* downstream to `bar`
|
|
* `baz` can *await* from `bar`/upstream
|
|
* You *run the pipeline* to perform effects/get a result
|
|
|
|
----
|
|
|
|
## Fusing
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
```
|
|
|
|
* Connect two components
|
|
* Output from upstream is the input to downstream
|
|
* Creates a new component of the two pieces fused together
|
|
* `.|` operator, or `fuse` function
|
|
|
|
----
|
|
|
|
## Streams
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
```
|
|
|
|
* `foo` sends a *stream* of values to `bar`
|
|
* The output from `foo` must match the input to `bar`
|
|
* Same thing with `bar` and `baz`
|
|
* `yield` to downstream
|
|
* `await` from upstream
|
|
|
|
----
|
|
|
|
## Results
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
```
|
|
|
|
* Single result value from a component
|
|
* When we fuse, throw away upstream result value
|
|
* Or use `fuseUpstream` or `fuseBoth`
|
|
* Example: `sumC`
|
|
* When we run the pipeline, this is the value that comes out
|
|
|
|
----
|
|
|
|
## Pipeline
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
```
|
|
|
|
* A complete pipeline does not have any meaningful input or output
|
|
* Input: unit value `()`
|
|
* Output: `Void`
|
|
* Why the difference? Let's talk over beers...
|
|
* Quiz:
|
|
* What's the input of `foo`?
|
|
* What's the output of `baz`?
|
|
|
|
----
|
|
|
|
## Conduit Execution
|
|
|
|
* Start at downstream
|
|
* Keep processing until it `await`s
|
|
* Pass control to next upstream component
|
|
* If upstream `await`s, keep going up the chain
|
|
* When we `yield`, pass control back downstream
|
|
* Downstream will always get control back
|
|
* Upstream: not so much
|
|
|
|
----
|
|
|
|
## Types
|
|
|
|
```haskell
|
|
runConduit $ foo .| bar .| baz
|
|
|
|
newtype ConduitM (i :: *) (o :: *) (m :: * -> *) (r :: *)
|
|
|
|
foo :: ConduitM () a m ()
|
|
bar :: ConduitM a b m ()
|
|
baz :: ConduitM b Void m r
|
|
|
|
foo .| bar :: ConduitM () b m ()
|
|
bar .| baz :: ConduitM b Void m r
|
|
|
|
foo .| bar .| baz :: ConduitM () Void m r
|
|
runConduit $ foo .| bar .| baz :: m r
|
|
```
|
|
|
|
----
|
|
|
|
## Example types
|
|
|
|
__NOTE__: In all cases, requires `Monad m`
|
|
|
|
```haskell
|
|
mapC :: (i -> o) -> ConduitM i o m ()
|
|
foldlC :: (r -> i -> r) -> r -> ConduitM i o m r
|
|
mapM_C :: (i -> m ()) -> ConduitM i o m ()
|
|
repeatC :: o -> ConduitM i o m ()
|
|
takeWhileC :: (i -> Bool) -> ConduitM i i m ()
|
|
decodeUtf8C :: MonadThrow m => Conduit ByteString m Text
|
|
```
|
|
|
|
---
|
|
|
|
## Congratulations!
|
|
|
|
* You now know all core concepts of Conduit
|
|
* Have a good day
|
|
|
|
----
|
|
|
|
## Just Kidding
|
|
|
|
![Deeper](http://i1.kym-cdn.com/photos/images/newsfeed/000/531/557/a88.jpg)
|
|
|
|
---
|
|
|
|
## Understanding Effects
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
loudYield :: forall i. Int -> ConduitM i Int IO ()
|
|
loudYield x = do
|
|
liftIO $ putStrLn $ "yielding: " ++ show x
|
|
yield x
|
|
loudSinkNull :: forall o. ConduitM Int o IO ()
|
|
loudSinkNull =
|
|
mapM_C $ \x -> putStrLn $ "awaited: " ++ show x
|
|
main =
|
|
runConduit $ mapM_ loudYield [1..3]
|
|
.| loudSinkNull
|
|
```
|
|
|
|
----
|
|
|
|
## Output
|
|
|
|
```
|
|
yielding: 1
|
|
received: 1
|
|
yielding: 2
|
|
received: 2
|
|
yielding: 3
|
|
received: 3
|
|
```
|
|
|
|
Notice how control bounces back and forth between components.
|
|
|
|
----
|
|
|
|
## Explicit await
|
|
|
|
```haskell
|
|
loudSinkNull =
|
|
loop
|
|
where
|
|
loop = do
|
|
liftIO $ putStrLn "calling await"
|
|
mx <- await
|
|
case mx of
|
|
Nothing -> liftIO $ putStrLn "all done!"
|
|
Just x -> do
|
|
liftIO $ putStrLn $ "received: " ++ show x
|
|
loop
|
|
```
|
|
|
|
```
|
|
calling await
|
|
yielding: 1
|
|
received: 1
|
|
calling await
|
|
yielding: 2
|
|
...
|
|
calling await
|
|
all done!
|
|
```
|
|
|
|
----
|
|
|
|
## No await
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
source = liftIO $ putStrLn "Entered the source"
|
|
sink = liftIO $ putStrLn "Entered the sink"
|
|
|
|
main = runConduit $ source .| sink
|
|
```
|
|
|
|
```
|
|
Entered the sink
|
|
```
|
|
|
|
Never entered the source!
|
|
|
|
----
|
|
|
|
## Guess the output
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
source = do
|
|
liftIO $ putStrLn "Source 1"
|
|
yield ()
|
|
liftIO $ putStrLn "Source 2"
|
|
|
|
sink = do
|
|
liftIO $ putStrLn "Sink 1"
|
|
_ <- await
|
|
liftIO $ putStrLn "Sink 2"
|
|
|
|
main = runConduit $ source .| sink
|
|
```
|
|
|
|
----
|
|
|
|
## Using undefined
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = runConduit $ undefined .| return ()
|
|
```
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = runConduit $ return () .| undefined .| return ()
|
|
```
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = runConduit $ return () .| undefined
|
|
```
|
|
|
|
---
|
|
|
|
## Finalizers
|
|
|
|
Upstream can't regain control, so...
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
source = do
|
|
liftIO $ putStrLn "acquire some resource"
|
|
mapM_ (\x -> yieldOr x
|
|
(putStrLn $ "cleaning up after: " ++ show x)
|
|
) [1..10]
|
|
|
|
main = runConduit $ source .| takeC 2 .| printC
|
|
```
|
|
|
|
```
|
|
acquire some resource
|
|
1
|
|
2
|
|
cleaning up after: 2
|
|
```
|
|
|
|
----
|
|
|
|
## Exceptions
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
source = do
|
|
liftIO $ putStrLn "acquire some resource"
|
|
mapM_ (\x -> yieldOr x
|
|
(putStrLn $ "cleaning up after: " ++ show x)
|
|
) [1..10]
|
|
|
|
main = runConduit $ source .| takeC 2 .| (printC >> undefined)
|
|
```
|
|
|
|
----
|
|
|
|
## ResourceT
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
source = bracketP
|
|
(putStrLn "acquire some resource")
|
|
(\() -> putStrLn "cleaning up")
|
|
(\() -> mapM_ yield [1..10])
|
|
|
|
main = runConduitRes
|
|
$ source .| takeC 2 .| (printC >> undefined)
|
|
```
|
|
|
|
----
|
|
|
|
## More on ResourceT
|
|
|
|
* Allows us to register cleanup events
|
|
* Occur even if exceptions are thrown
|
|
* Works around limitations of coroutine/CPS
|
|
* Simple cases can be replaced with bracket-pattern
|
|
* Some more complicated cases require something like `ResourceT`
|
|
* E.g., deep directory traversal
|
|
|
|
---
|
|
|
|
## Average (bad)
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = print
|
|
$ runConduitPure
|
|
$ yieldMany [1..10 :: Double]
|
|
.| ((/)
|
|
<$> sumC
|
|
<*> (fromIntegral <$> lengthC))
|
|
```
|
|
|
|
----
|
|
|
|
## Average (good)
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = print
|
|
$ runConduitPure
|
|
$ yieldMany [1..10 :: Double]
|
|
.| getZipSink ((/)
|
|
<$> ZipSink sumC
|
|
<*> ZipSink (fromIntegral <$> lengthC))
|
|
```
|
|
|
|
Nice perk: Conduit forced us to avoid a common space leak in the list
|
|
version!
|
|
|
|
----
|
|
|
|
## Takeaways
|
|
|
|
* `Applicative` and `Monad` composition sequentially consumes upstream
|
|
* They also sequentially produce downstream
|
|
* `ZipSink` allows them to consume in parallel
|
|
|
|
---
|
|
|
|
## Folds
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = print
|
|
$ runConduitPure
|
|
$ yieldMany [1..10]
|
|
.| foldlC (flip (:)) []
|
|
```
|
|
|
|
----
|
|
|
|
## Monadic folds
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = runConduit
|
|
$ yieldMany [1..10]
|
|
.| (foldMC f 0 >>= liftIO . print)
|
|
where
|
|
f total x = do
|
|
putStrLn $ "Received: " ++ show x
|
|
return $ total + x
|
|
```
|
|
|
|
---
|
|
|
|
## Chunked data
|
|
|
|
What's wrong with this picture?
|
|
|
|
```haskell
|
|
sinkHistogram
|
|
:: Monad m
|
|
=> ConduitM Word8 o m (HM.HashMap Word8 Int)
|
|
sinkHistogram =
|
|
foldlC go HM.empty
|
|
where
|
|
go m w = HM.insertWith (+) w 1 m
|
|
```
|
|
|
|
* Conduit does introduce an overhead
|
|
* An extra `await`/`yield` per byte is _heavy_
|
|
|
|
----
|
|
|
|
## Much better
|
|
|
|
```haskell
|
|
sinkHistogram
|
|
:: Monad m
|
|
=> ConduitM ByteString o m (HM.HashMap Word8 Int)
|
|
sinkHistogram =
|
|
foldlCE go HM.empty
|
|
where
|
|
go m w = HM.insertWith (+) w 1 m
|
|
```
|
|
|
|
* All we did was replace `foldlC` with `foldlCE`
|
|
* More generalized type signature:
|
|
|
|
```haskell
|
|
sinkHistogram
|
|
:: (Monad m, Element i ~ Word8, MonoFoldable i)
|
|
=> ConduitM i o m (HM.HashMap Word8 Int)
|
|
```
|
|
|
|
---
|
|
|
|
## Leftovers
|
|
|
|
Guess the output
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
import Conduit
|
|
|
|
main = print
|
|
$ runConduitPure
|
|
$ yieldMany [1 .. 10 :: Int]
|
|
.| ((,)
|
|
<$> (takeWhileC (< 6) .| sinkList)
|
|
<*> sinkList)
|
|
```
|
|
|
|
(Not a trick question... yet)
|
|
|
|
```
|
|
([1,2,3,4,5],[6,7,8,9,10])
|
|
```
|
|
|
|
----
|
|
|
|
## Let's implement takeWhileC
|
|
|
|
```haskell
|
|
myTakeWhileC :: Monad m
|
|
=> (i -> Bool)
|
|
-> ConduitM i i m ()
|
|
myTakeWhileC f =
|
|
loop
|
|
where
|
|
loop = do
|
|
mx <- await
|
|
case mx of
|
|
Nothing -> return ()
|
|
Just x
|
|
| f x -> yield x >> loop
|
|
| otherwise -> return ()
|
|
```
|
|
|
|
Hmm...
|
|
|
|
```
|
|
([1,2,3,4,5],[7,8,9,10])
|
|
```
|
|
|
|
----
|
|
|
|
## Let's fix that
|
|
|
|
```haskell
|
|
myTakeWhileC :: Monad m
|
|
=> (i -> Bool)
|
|
-> ConduitM i i m ()
|
|
myTakeWhileC f =
|
|
loop
|
|
where
|
|
loop = do
|
|
mx <- await
|
|
case mx of
|
|
Nothing -> return ()
|
|
Just x
|
|
| f x -> yield x >> loop
|
|
| otherwise -> leftover x
|
|
```
|
|
|
|
----
|
|
|
|
## More leftovers examples
|
|
|
|
Let's step it up a notch
|
|
|
|
```haskell
|
|
main = runConduit
|
|
$ yieldMany [1 .. 10 :: Int]
|
|
.| do
|
|
mapC id .| (await >>= maybe (return ()) leftover)
|
|
printC
|
|
.| do
|
|
leftover "Hello There!"
|
|
printC
|
|
```
|
|
|
|
* (Output on next slide)
|
|
* Don't forget: start downstream when processing!
|
|
* Yes, you can deeply nest Conduit components like this
|
|
|
|
----
|
|
|
|
## Output from previous slides
|
|
|
|
```
|
|
"Hello There!"
|
|
2
|
|
3
|
|
4
|
|
5
|
|
6
|
|
7
|
|
8
|
|
9
|
|
10
|
|
```
|
|
|
|
----
|
|
|
|
## Leftover lessons
|
|
|
|
* Whenever you use `leftover`, the next monadic bind picks up the
|
|
value with `await`
|
|
* Fusion drops any leftovers (they can't be passed upstream)
|
|
* If needed, use `fuseLeftovers`
|
|
* This is the primary reason Conduit isn't a category
|
|
* Leftovers especially useful for chunked data, e.g.
|
|
* Read a `ByteString`
|
|
* Consume part of the `ByteString`
|
|
* Use `leftover` on the rest
|
|
|
|
---
|
|
|
|
## Library ecosystem
|
|
|
|
* Lots of different packages
|
|
* `conduit` provides core datatypes and basic functions
|
|
* `conduit-extra` has commonly used helpers
|
|
* `conduit-combinators`: batteries-included, chunked and unchunked
|
|
|
|
----
|
|
|
|
## My recommendation
|
|
|
|
* Use `conduit-combinators` by default
|
|
* Import `Conduit` which doesn't require qualified import
|
|
* Most names have `C` as a suffix (e.g., `foldlC`)
|
|
* Chunked versions have a `CE` suffix (for *element*, e.g., `foldlCE`)
|
|
|
|
---
|
|
|
|
# Stretch
|
|
|
|
Prepare yourselves for Yesod :)
|
|
|
|
---
|
|
|
|
## Yesod
|
|
|
|
* Web framework
|
|
* Supports traditional HTML sites and web services (usually JSON)
|
|
* Goal: turn as many common bugs into compile-time errors
|
|
* Philosophy: bring the benefits of Haskell to a standard MVC-ish framework
|
|
|
|
----
|
|
|
|
## How it works
|
|
|
|
* Built on Web Application Interface (WAI)
|
|
* Template Haskell + DSL for type-safe routing
|
|
* `Handler` monad for coding routes
|
|
* `Widget`s and templates for HTML/CSS/JS
|
|
* Many add-on libraries for common tasks (auth, forms, XML sitemaps)
|
|
* Ties in well with Persistent for type-safe database access
|
|
|
|
----
|
|
|
|
## Flexibility
|
|
|
|
* Yesod is more flexible than we'll discuss today
|
|
* Template Haskell, DSLs aren't required
|
|
* Swap out database libraries
|
|
* Host with FastCGI instead of Warp
|
|
* For those interested: http://www.yesodweb.com/book/yesod-for-haskellers
|
|
|
|
----
|
|
|
|
## "Standard" workflow
|
|
|
|
* Scaffolded site: `stack new mysite yesod-postgres`
|
|
* Built in:
|
|
* Auth
|
|
* Config file + env vars
|
|
* HTML templating + Bootstrap.css
|
|
* Logging
|
|
* CSS minification
|
|
* Development server (`yesod devel`)
|
|
|
|
----
|
|
|
|
## What we'll cover today
|
|
|
|
* Yesod is _big_
|
|
* Focus today on mostly JSON services subset
|
|
* Thanks to Kris Nuttycombe for this suggestion :)
|
|
* Want more? Talk to me after, or check out the book
|
|
http://www.yesodweb.com/book
|
|
|
|
---
|
|
|
|
## Common Stuff
|
|
|
|
```haskell
|
|
#!/usr/bin/env stack
|
|
-- stack --resolver lts-8.12 script
|
|
{-# LANGUAGE OverloadedStrings, QuasiQuotes TemplateHaskell,
|
|
TypeFamilies, NoImplicitPrelude, ViewPatterns #-}
|
|
import ClassyPrelude.Yesod
|
|
data App = App
|
|
mkYesod "App" [parseRoutes|
|
|
...
|
|
|]
|
|
instance Yesod App
|
|
...
|
|
main = warp 3000 App
|
|
```
|
|
|
|
----
|
|
|
|
## Language extensions
|
|
|
|
* Yesod uses a bunch
|
|
* Use Persistent? That's a paddlin'
|
|
* `OverloadedStrings`? Duh
|
|
* `QuasiQuotes` and `TemplateHaskell` for routing DSL
|
|
* `TypeFamilies` are used for associated route types
|
|
* `NoImplicitPrelude` because we're using ClassyPrelude
|
|
* `ViewPatterns` is part of the generated parsing code
|
|
|
|
----
|
|
|
|
## Imports
|
|
|
|
```haskell
|
|
import ClassyPrelude.Yesod
|
|
```
|
|
|
|
* Some men just like to watch the world burn
|
|
* Also, convenient to avoid a bunch of imports in these slides
|
|
|
|
----
|
|
|
|
## Foundation data type
|
|
|
|
```haskell
|
|
data App = App
|
|
```
|
|
|
|
* Every app has a central data type
|
|
* Put config values, globals, etc, in it in your `main` function
|
|
* Access value from any `Handler` with `getYesod`
|
|
* Also used for associated route types
|
|
|
|
----
|
|
|
|
## Route definition and `mkYesod`
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
...
|
|
|]
|
|
```
|
|
|
|
* Define your routes with a DSL
|
|
* Generates a data type for your routes
|
|
* Also generates some convenience type synonyms
|
|
|
|
----
|
|
|
|
## Route example
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET
|
|
|]
|
|
```
|
|
|
|
Generates
|
|
|
|
```haskell
|
|
instance RenderRoute App where
|
|
data Route App = HomeR
|
|
renderRoute :: Route App -> ([Text], [(Text, Text)])
|
|
instance ParseRoute App where
|
|
parseRoute :: ([Text], [(Text, Text)]) -> Maybe (Route App)
|
|
type Handler = HandlerT IO App
|
|
instance YesodDispatch App
|
|
```
|
|
|
|
* And a few others
|
|
* Goal: hide away tedious, error-prone boilerplate
|
|
|
|
----
|
|
|
|
## Yesod typeclass
|
|
|
|
```haskell
|
|
instance Yesod App
|
|
```
|
|
|
|
* Collection of overridable settings
|
|
* Example: how to store user session data
|
|
* Defaults are Good Enough™ in many cases
|
|
* Scaffolded site helps a lot
|
|
|
|
----
|
|
|
|
## Defining your Handlers
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET
|
|
/fibs/#Int FibsR GET
|
|
|]
|
|
|
|
getHomeR :: Handler Text
|
|
getFibsR :: Int -> Handler Value
|
|
```
|
|
|
|
* Handler names determined by convention
|
|
* Often mime-type determined by return type
|
|
* `YesodDispatch` instance uses these functions
|
|
|
|
----
|
|
|
|
## Run it!
|
|
|
|
```haskell
|
|
main :: IO ()
|
|
main = warp 3000 App
|
|
```
|
|
|
|
* `warp` is a convenient helper
|
|
* Performs any initialization necessary (specified in `Yesod` instance)
|
|
* Converts to a WAI `Application`
|
|
* Runs on given port with Warp
|
|
* Installs some standard middlewares
|
|
* `toWaiApp` or `toWaiAppPlain` == more control
|
|
* Can perform initialization before `warp` call
|
|
|
|
---
|
|
|
|
## Hello World
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET
|
|
|]
|
|
|
|
getHomeR :: Handler Text
|
|
getHomeR = return "Hello World!"
|
|
```
|
|
|
|
* Only responds to `/`
|
|
* Responds with a `text/plain` mime type
|
|
|
|
----
|
|
|
|
## JSON output
|
|
|
|
```haskell
|
|
getHomeR :: Handler Value
|
|
getHomeR = return "Hello World!"
|
|
```
|
|
|
|
* Notice the difference?
|
|
* `Value` type determines `application/json`
|
|
|
|
----
|
|
|
|
## Why not both?
|
|
|
|
```haskell
|
|
getHomeR :: Handler TypedContent
|
|
getHomeR = selectRep $ do
|
|
provideRep $ return ("Hello World!" :: Text)
|
|
provideRep $ return ("Hello World!" :: Value)
|
|
```
|
|
|
|
* Types determine mime per representation
|
|
* No accept header: use first
|
|
* Otherwise, fiinds match
|
|
* No match: returns a `406 Not Acceptable`
|
|
|
|
----
|
|
|
|
## Arbitrary mime types
|
|
|
|
```haskell
|
|
getHomeR :: Handler TypedContent
|
|
getHomeR = selectRep $ do
|
|
provideRep $ return ("Hello World!" :: Text)
|
|
provideRep $ return ("Hello World!" :: Value)
|
|
provideRepType "text/csv" $ return ("hello,world\n" :: Text)
|
|
```
|
|
|
|
---
|
|
|
|
## Route parameters
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET
|
|
/fibs/#Int FibsR GET
|
|
|]
|
|
|
|
getHomeR :: Handler ()
|
|
getHomeR = redirect $ FibsR 1
|
|
|
|
getFibsR :: Int -> Handler Value
|
|
getFibsR i = do
|
|
render <- getUrlRender
|
|
return $ object
|
|
[ "value" .= (fibs !! i)
|
|
, "next" .= render (FibsR (i + 1)) ]
|
|
```
|
|
|
|
* Values is parsed and passed into the handler
|
|
* Route type makes data cons with arguments
|
|
|
|
----
|
|
|
|
## Query string parameters
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ FibsR GET
|
|
|]
|
|
|
|
getFibsR :: Handler Value
|
|
getFibsR = do
|
|
mi <- lookupGetParam "index"
|
|
let i = fromMaybe 1 $ mi >>= readMay . unpack
|
|
render <- getUrlRenderParams
|
|
return $ object
|
|
[ "value" .= (fibs !! i)
|
|
, "next" .= render FibsR [("index", tshow (i + 1))]
|
|
]
|
|
```
|
|
|
|
* We love fibs :)
|
|
* Lookup parameters easily (also: forms support)
|
|
* Render URLs with and without parameter lists
|
|
|
|
----
|
|
|
|
## POST parameters
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ FibsR PUT
|
|
|]
|
|
|
|
putFibsR :: Handler Value
|
|
putFibsR = do
|
|
mi <- lookupPostParam "index"
|
|
let i = fromMaybe 1 $ mi >>= readMay . unpack
|
|
return $ object
|
|
[ "value" .= (fibs !! i)
|
|
]
|
|
```
|
|
|
|
curl -i http://localhost:3000/ -X PUT -d index=4
|
|
|
|
* `PUT` method, but still call them POST params :(
|
|
* Again, form support is available
|
|
|
|
----
|
|
|
|
## POST files
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR PUT
|
|
|]
|
|
|
|
putHomeR :: Handler Value
|
|
putHomeR = do
|
|
Just fileInfo <- lookupFile "some-file"
|
|
size <- runConduitRes $ fileSource fileInfo .| lengthCE
|
|
return $ object
|
|
[ "name" .= fileName fileInfo
|
|
, "content-type" .= fileContentType fileInfo
|
|
, "size" .= (size :: Int)
|
|
]
|
|
```
|
|
|
|
curl -i http://localhost:3000/ -X PUT -F some-file=@image.png
|
|
|
|
* Yay conduit!
|
|
* Want all POST info? `runRequestBody`
|
|
|
|
----
|
|
|
|
## JSON request body
|
|
|
|
```haskell
|
|
putHomeR :: Handler Value
|
|
putHomeR = requireCheckJsonBody
|
|
```
|
|
|
|
curl -i http://localhost:3000/ -X PUT \
|
|
-H "Content-Type:application/json" \
|
|
-d '{"foo":"bar"}'
|
|
|
|
* Dumb echo server
|
|
* Uses any `FromJSON` instance
|
|
* `Check` says "check mime-type before parsing"
|
|
* Backwards compat can be annoying :)
|
|
|
|
---
|
|
|
|
## Header echo
|
|
|
|
```haskell
|
|
getHomeR :: Handler ()
|
|
getHomeR = do
|
|
mvalue <- lookupHeader "marco"
|
|
forM_ mvalue $ addHeader "polo" . decodeUtf8
|
|
```
|
|
|
|
curl -i http://localhost:3000/ -H "Marco:Hello"
|
|
|
|
* Case-insensitive lookup
|
|
* Text vs ByteString: yes, it's annoying
|
|
* Oh, yes, you can just return unit
|
|
|
|
---
|
|
|
|
## Permissions
|
|
|
|
```haskell
|
|
getHomeR :: Handler Text
|
|
getHomeR = do
|
|
mpassword <- lookupGetParam "password"
|
|
case mpassword of
|
|
Just "12345" -> return "Hello President Skroob"
|
|
_ -> permissionDenied "Self Destruct Initiated"
|
|
```
|
|
|
|
* Don't actually use GET params for passwords
|
|
|
|
----
|
|
|
|
## Route Attributes and `isAuthorized`
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET !admin
|
|
|]
|
|
instance Yesod App where
|
|
isAuthorized route _isWrite
|
|
| "admin" `member` routeAttrs route = do
|
|
mpassword <- lookupGetParam "password"
|
|
case mpassword of
|
|
Just "12345" -> return Authorized
|
|
_ -> return $ Unauthorized "Self Destruct Initiated"
|
|
| otherwise = return Authorized
|
|
getHomeR :: Handler Text
|
|
getHomeR = return "Hello President Skroob"
|
|
```
|
|
|
|
Separate those concerns!
|
|
|
|
----
|
|
|
|
## Session values
|
|
|
|
```haskell
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR GET !admin
|
|
/auth AuthR POST
|
|
|]
|
|
|
|
getHomeR :: Handler Text
|
|
getHomeR = return "Hello President Skroob"
|
|
|
|
postAuthR :: Handler ()
|
|
postAuthR = do
|
|
mpassword <- lookupPostParam "password"
|
|
case mpassword of
|
|
Just "12345" -> setSession "AUTH" "Yes"
|
|
_ -> permissionDenied "Self Destruct Initiated"
|
|
```
|
|
|
|
* Sets a key to a value in the user session
|
|
* Default: HMAC-secured client session key in a cookie
|
|
* Code continues...
|
|
|
|
----
|
|
|
|
## Session based auth functions
|
|
|
|
```haskell
|
|
instance Yesod App where
|
|
authRoute _ = Just AuthR
|
|
isAuthorized route _isWrite
|
|
| "admin" `member` routeAttrs route = do
|
|
mauth <- lookupSession "AUTH"
|
|
case mauth of
|
|
Just "Yes" -> return Authorized
|
|
_ -> return AuthenticationRequired
|
|
| otherwise = return Authorized
|
|
```
|
|
|
|
* `authRoute` is where users are redirected
|
|
* In a full app: `GET AuthR` would give a user-friendly page
|
|
|
|
----
|
|
|
|
## Real world auth
|
|
|
|
* yesod-auth provides lots of backends
|
|
* OpenID, Google Email, local email...
|
|
* I personally really like third party auth
|
|
* Still sad that Mozilla Persona shut down
|
|
|
|
---
|
|
|
|
## Streaming request body
|
|
|
|
```haskell
|
|
import Text.XML.Stream.Parse
|
|
|
|
mkYesod "App" [parseRoutes|
|
|
/ HomeR PUT
|
|
|]
|
|
|
|
putHomeR :: Handler Value
|
|
putHomeR = do
|
|
events <- runConduit $ rawRequestBody .| parseBytes def .| lengthC
|
|
return $ object ["event-count" .= (events :: Int)]
|
|
```
|
|
|
|
* Conduit to the rescue
|
|
* Request body = stream of `ByteString`
|
|
* Request body can be consumed once!
|
|
|
|
----
|
|
|
|
## Streaming response body
|
|
|
|
```haskell
|
|
import Data.ByteString.Builder (intDec)
|
|
|
|
getHomeR :: Handler TypedContent
|
|
getHomeR = respondSource "text/csv" $ do
|
|
yield $ Chunk "number,plus1\n"
|
|
forM_ [1..100 :: Int] $ \i -> yield
|
|
$ Chunk $ intDec i <> "," <> intDec (i + 1) <> "\n"
|
|
```
|
|
|
|
* Again with the conduits
|
|
* Use the `data Flush a = Flush | Chunk a` type
|
|
* ByteString Builders under the surface
|
|
* A little tedious, so...
|
|
|
|
----
|
|
|
|
## Convenient streaming functions
|
|
|
|
```haskell
|
|
getHomeR :: Handler TypedContent
|
|
getHomeR = respondSource "text/csv" $ do
|
|
sendChunkText "number,plus1\n"
|
|
forM_ [1..100 :: Int] $ \i -> sendChunkText $ mconcat
|
|
[ tshow i
|
|
, ","
|
|
, tshow (i + 1)
|
|
, "\n"
|
|
]
|
|
```
|
|
|
|
* Avoid need to use explicit `Chunk` constructor
|
|
* Use `Text` or `ByteString` instead of `Builder`
|
|
|
|
---
|
|
|
|
## Config files (1)
|
|
|
|
```yaml
|
|
aws-secret: _env:AWS_SECRET
|
|
home-response: _env:HOME_RESPONSE:Hello World
|
|
```
|
|
|
|
```haskell
|
|
data Config = Config
|
|
{ awsSecret :: !Text
|
|
, homeResponse :: !Text
|
|
}
|
|
instance FromJSON Config where
|
|
parseJSON = withObject "Config" $ \o -> Config
|
|
<$> o .: "aws-secret"
|
|
<*> o .: "home-response"
|
|
```
|
|
|
|
* Special syntax in YAML to allow env overriding
|
|
* aws-secret: must have an env var
|
|
* home-response: optional
|
|
* `FromJSON`: normal aeson code
|
|
|
|
----
|
|
|
|
## Config files (2)
|
|
|
|
```haskell
|
|
data App = App !Config
|
|
|
|
getHomeR :: Handler Text
|
|
getHomeR = do
|
|
App config <- getYesod
|
|
return $ homeResponse config
|
|
|
|
main :: IO ()
|
|
main = do
|
|
config <- loadYamlSettingsArgs [] useEnv
|
|
warp 3000 $ App config
|
|
```
|
|
|
|
* Stick `Config` inside `App`
|
|
* Get `Config` with `getYesod`
|
|
* Initialize `Config` with `loadYamlSettingsArgs`
|
|
|
|
----
|
|
|
|
## Config files (3)
|
|
|
|
```
|
|
$ ./Main.hs
|
|
Main.hs: loadYamlSettings: No configuration provided
|
|
$ ./Main.hs config.yaml
|
|
Main.hs: Could not convert to AppSettings: expected Text,
|
|
encountered Null
|
|
$ AWS_SECRET=foobar ./Main.hs config.yaml
|
|
Application launched ^C
|
|
$ AWS_SECRET=foobar HOME_RESPONSE=Goodbye \
|
|
./Main.hs config.yaml
|
|
Application launched ^C
|
|
```
|
|
|
|
* Must provide config file(s) on command line
|
|
* Must provide `AWS_SECRET`
|
|
* If provided, `HOME_RESPONSE` changes that response payload
|
|
|
|
---
|
|
|
|
## Learn More
|
|
|
|
* http://www.yesodweb.com/
|
|
* https://github.com/yesodweb/yesod-cookbook
|
|
* Example code bases
|
|
* https://github.com/snoyberg/haskellers
|
|
* https://github.com/yesodweb/yesodweb.com
|