505 lines
16 KiB
Org Mode
505 lines
16 KiB
Org Mode
|
#+Title: Haskell Web Application from scratch
|
||
|
#+Author: Yann Esposito
|
||
|
|
||
|
* Introduction
|
||
|
|
||
|
** Functional Programming oriented to make a web application
|
||
|
|
||
|
** Tooling choices
|
||
|
|
||
|
- macOS Sierra
|
||
|
- spacemacs
|
||
|
- stack (not using ghc-8.0.1, there is a bug with macOS)
|
||
|
|
||
|
** High quality application
|
||
|
|
||
|
Even if an application only print "hello world" there are a lot of subtle way
|
||
|
such an app could fail or have problems. See for example the [changelogs to GNU
|
||
|
Hello](https://github.com/avar/gnu-hello/blob/master/ChangeLog).
|
||
|
|
||
|
The goal of this tutorial is not to provide a "see what we can do with Haskell"
|
||
|
but more, how could we enforce production quality development with Haskell.
|
||
|
Unfortunately, the tooling is very important in these matters.
|
||
|
|
||
|
To reach such goal we should at least provide:
|
||
|
|
||
|
- Documentation
|
||
|
- Unit Tests
|
||
|
- Generative Tests
|
||
|
- Benchmarks
|
||
|
- Profiling
|
||
|
- CI
|
||
|
- Deployment
|
||
|
|
||
|
It's easy to have one tutorial for each of these concepts, here that won't be a
|
||
|
deep dive, but a first introduction on how to achieve all these goals.
|
||
|
|
||
|
* Tooling and preparing to code
|
||
|
|
||
|
blog-image("stillsuit.jpg","Stillsuit")
|
||
|
|
||
|
** Warning
|
||
|
|
||
|
If you never installed Haskell before, it should be a bit long to setup a
|
||
|
correct working environment. So please follow me, don't give up because
|
||
|
something doesn't work the first time. I made my best to make my environment
|
||
|
work for most people.
|
||
|
|
||
|
** Installing Haskell Compiler
|
||
|
|
||
|
Install Haskell etc... In my opinion the easiest way to start is to install =stack=.
|
||
|
Then you need to choose a great name for your project, why not =shai-hulud=?
|
||
|
|
||
|
blog-image("shai-hulud.jpg","Shai Hulud")
|
||
|
|
||
|
#+BEGIN_SRC bash
|
||
|
> stack new shai-hulud tasty-travis
|
||
|
#+END_SRC
|
||
|
|
||
|
Yeah now you have a new directory, let use git:
|
||
|
|
||
|
#+BEGIN_SRC bash
|
||
|
> cd shai-hulud
|
||
|
> git init .
|
||
|
#+END_SRC
|
||
|
|
||
|
Now we have some source code, let's try it[^1].
|
||
|
|
||
|
[^1]: If you are on a Mac, please, modify the line =resolver: lts-7.18= by
|
||
|
=resolver: nightly-2017-01-25= to be certain to use =ghc-8.0.2= because there is
|
||
|
a bug with =ghc-8.0.1=.
|
||
|
|
||
|
#+BEGIN_SRC bash
|
||
|
> stack setup && stack build && stack exec -- shai-hulud-exe
|
||
|
42
|
||
|
#+END_SRC
|
||
|
|
||
|
** Dependencies & Libraries
|
||
|
|
||
|
As we want to make a web application let's add the needed dependencies to help
|
||
|
us. Typically we want a web server
|
||
|
[warp](https://hackage.haskell.org/package/warp) and also a Web Application
|
||
|
Interface [WAI](https://hackage.haskell.org/package/wai). We'll also need to use
|
||
|
[http-types](https://hackage.haskell.org/package/http-types).
|
||
|
|
||
|
In the =shai-hulud.cabal= file, in the =shai-hulud-exe= section, add to the
|
||
|
build-depends =http-types=, =wai= and =warp= like this:
|
||
|
|
||
|
#+BEGIN_SRC
|
||
|
executable shai-hulud-exe
|
||
|
default-language: Haskell2010
|
||
|
ghc-options: -Wall -Werror -O2 -threaded -rtsopts -with-rtsopts=-N
|
||
|
hs-source-dirs: src-exe
|
||
|
main-is: Main.hs
|
||
|
build-depends: base >= 4.8 && < 5
|
||
|
, shai-hulud
|
||
|
, http-types
|
||
|
, wai
|
||
|
, warp
|
||
|
#+END_SRC
|
||
|
|
||
|
Then we modify the =src-exe/Main.hs= file to contains:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
{-# LANGUAGE OverloadedStrings #-}
|
||
|
import Network.Wai
|
||
|
import Network.HTTP.Types
|
||
|
import Network.Wai.Handler.Warp (run)
|
||
|
|
||
|
app :: Application
|
||
|
app _ respond = do
|
||
|
putStrLn "I've done some IO here"
|
||
|
respond $ responseLBS status200 [("Content-Type","text/plain")] "Hello, Web!"
|
||
|
|
||
|
main :: IO ()
|
||
|
main = do
|
||
|
putStrLn "http://localhost:8080/"
|
||
|
run 8080 app
|
||
|
#+END_SRC
|
||
|
|
||
|
We'll go in detail later about what everything means.
|
||
|
|
||
|
#+BEGIN_SRC bash
|
||
|
> stack build && stack exec -- shai-hulud-exe
|
||
|
...
|
||
|
... Lot of building logs there
|
||
|
...
|
||
|
http://localhost:8080/
|
||
|
#+END_SRC
|
||
|
|
||
|
Yeah! It appears to work, now let's try it by going on <http://localhost:8080/>
|
||
|
in a web browser. You should see =Hello, Web!= in your browser and each time you
|
||
|
reload the page a new message is printed in the console because some IO were performed.
|
||
|
|
||
|
** So can we start yet?
|
||
|
|
||
|
Hmmm no sorry, not yet.
|
||
|
|
||
|
|
||
|
We should not use the default prelude.
|
||
|
|
||
|
While this article is a tutorial, it is not exactly a "very basic" one. I mean,
|
||
|
once finished the environment would be good enough for production. There will be
|
||
|
tests, ability to reproduce the build on a CI, and so, for such a program I
|
||
|
should prevent it to have runtime errors.
|
||
|
|
||
|
In fact, certainly one of the main reason to use Haskell is that it helps
|
||
|
prevent runtime errors.
|
||
|
|
||
|
In order to do that we'll use a prelude that doesn't contain any partial
|
||
|
functions. So I choosed to use =protolude=[^2].
|
||
|
|
||
|
|
||
|
For that that's quite easy, simply add =protolude= as a dependency to your cabal file.
|
||
|
We'll modify the cabal file that way:
|
||
|
|
||
|
#+BEGIN_SRC
|
||
|
library
|
||
|
default-language: Haskell2010
|
||
|
ghc-options: -Wall -Werror -O2
|
||
|
hs-source-dirs: src
|
||
|
exposed-modules: {-# higlight #-}Lib{-# /highlight #-}
|
||
|
, ShaiHulud.App
|
||
|
build-depends: base >= 4.8 && < 5
|
||
|
, http-types
|
||
|
, protolude
|
||
|
, wai
|
||
|
|
||
|
executable shai-hulud-exe
|
||
|
default-language: Haskell2010
|
||
|
ghc-options: -Wall -Werror -O2 -threaded -rtsopts -with-rtsopts=-N
|
||
|
hs-source-dirs: src-exe
|
||
|
main-is: Main.hs
|
||
|
build-depends: shai-hulud
|
||
|
, base >= 4.8 && < 5
|
||
|
, http-types
|
||
|
, protolude
|
||
|
, wai
|
||
|
, warp
|
||
|
#+END_SRC
|
||
|
|
||
|
We move the =app= declaration in =src/ShaiHulud/App.hs=:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||
|
{-# LANGUAGE OverloadedStrings #-}
|
||
|
|
||
|
module ShaiHulud.App
|
||
|
( app )
|
||
|
where
|
||
|
|
||
|
import Protolude
|
||
|
|
||
|
import Network.Wai
|
||
|
import Network.HTTP.Types
|
||
|
|
||
|
app :: Application
|
||
|
app _ respond = do
|
||
|
putText "I've done some IO here"
|
||
|
respond $ responseLBS status200 [("Content-Type","text/plain")] "Hello, Web!"
|
||
|
#+END_SRC
|
||
|
|
||
|
And we remove it from =src-exe/Main.hs=:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||
|
{-# LANGUAGE OverloadedStrings #-}
|
||
|
import Protolude
|
||
|
|
||
|
import Network.Wai.Handler.Warp (run)
|
||
|
|
||
|
import ShaiHulud.App (app)
|
||
|
|
||
|
main :: IO ()
|
||
|
main = do
|
||
|
putText "http://localhost:8080/"
|
||
|
run 8080 app
|
||
|
#+END_SRC
|
||
|
|
||
|
So now the tooling around being able to start working seems done.
|
||
|
|
||
|
** Not yet
|
||
|
|
||
|
Yes I talked about:
|
||
|
- Installation with =stack= that should take care of installing Haskell
|
||
|
- How to add dependencies by adding them to the cabal file
|
||
|
- Sane prelude with =protolude=
|
||
|
- Provided an overview of WAI Application type
|
||
|
|
||
|
But I forgot to mention part of the tooling that is generally very personal.
|
||
|
I use spacemacs and to take advantages of many of the editor niceties
|
||
|
I also use =intero= and =haddock=.
|
||
|
|
||
|
So other things to think about:
|
||
|
|
||
|
- Install =intero= with =stack install intero=.
|
||
|
- Also generate hoogle documentation: =stack hoogle data=
|
||
|
- You could also check the tests and benchmark suites: =stack test= and =stack bench=
|
||
|
|
||
|
** So we should be done with prelimiaries
|
||
|
|
||
|
So we should be done with preliminaries, at least, I hope so...
|
||
|
|
||
|
If you started from scratch it was certainly a terrible first experience. But
|
||
|
be assured that once done, most of the step you've taken won't be needed for your next
|
||
|
project.
|
||
|
|
||
|
* Web Application
|
||
|
|
||
|
So what is a web application?
|
||
|
|
||
|
** WAI
|
||
|
|
||
|
So if you look again at the code you see that your application main function
|
||
|
simply print =http://localhost:8080/= and then run the server on the port =8080=
|
||
|
using =app=.
|
||
|
|
||
|
The type of =app= is =Application=, if we look at the type of Application in
|
||
|
WAI, for example by using =SPC-h-h= on the Application keyword or by going in
|
||
|
the [WAI documentation](https://www.stackage.org/haddock/lts-7.18/wai-3.2.1.1/Network-Wai.html).
|
||
|
|
||
|
We see that:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
|
||
|
#+END_SRC
|
||
|
|
||
|
Hmmmm.... What? So just remakr WAI is at it's third major version. So if we just
|
||
|
take a look at WAI in its previous version we see that Application was defined
|
||
|
as:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
type Application = Request -> IO Response
|
||
|
#+END_SRC
|
||
|
|
||
|
Which look quite more intuitive. Because, what is the role of a web server if
|
||
|
not sending response to requests? The IO here is just there to explain that in
|
||
|
order to send a response the server might use IOs like reading in some DB or the
|
||
|
file system.
|
||
|
|
||
|
So why let's take a bit to analyze the new definition of =Application= in WAI 3.
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
|
||
|
#+END_SRC
|
||
|
|
||
|
It is explained:
|
||
|
|
||
|
#+BEGIN_QUOTE
|
||
|
The WAI application.
|
||
|
|
||
|
Note that, since WAI 3.0, this type is structured in continuation passing style
|
||
|
to allow for proper safe resource handling. This was handled in the past via
|
||
|
other means (e.g., ResourceT). As a demonstration:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
app :: Application
|
||
|
app req respond = bracket_
|
||
|
(putStrLn "Allocating scarce resource")
|
||
|
(putStrLn "Cleaning up")
|
||
|
(respond $ responseLBS status200 [] "Hello World")
|
||
|
#+END_SRC
|
||
|
|
||
|
#+END_QUOTE
|
||
|
|
||
|
Great, so before it was difficult to handling some resources, now it appears to
|
||
|
be easier to write using =bracket_=. Hmm... =bracket_=? What is this function?
|
||
|
If you search it in [hoogle](https://www.haskell.org/hoogle/?hoogle=bracket_):
|
||
|
|
||
|
OK that's quite easy, you see it is a function of =Control.Exception.Base= that
|
||
|
we could use like this:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
bracket
|
||
|
(openFile "filename" ReadMode)
|
||
|
(hClose)
|
||
|
(\fileHandle -> do { ... })
|
||
|
#+END_SRC
|
||
|
|
||
|
And =bracket_= is a variation of =bracket= which doesn't need the return value
|
||
|
of the first computation to be used the the "closing" computation.
|
||
|
(More details here)[http://hackage.haskell.org/package/base-4.9.1.0/docs/Control-Exception-Base.html#v:bracket_].
|
||
|
|
||
|
So ok, an =Application= is "mostly" a function that take a =Request= an returns
|
||
|
an =IO Response=.
|
||
|
|
||
|
Good, now let's take another look to the =app= code:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
app :: Application
|
||
|
app _ respond = do
|
||
|
putText "I've done some IO here"
|
||
|
respond $ responseLBS status200 [("Content-Type","text/plain")] "Hello, Web!"
|
||
|
#+END_SRC
|
||
|
|
||
|
As you see we don't use the first parameter, the =Request=. So we could ask for
|
||
|
some JSON data on =/foo/bar/= with a POST, it will still respond an HTTP 200
|
||
|
with content-type plain text containing the body =Hello, Web!=.
|
||
|
|
||
|
So what a web app should provide. And here we could go down the rabbit hole of
|
||
|
the HTTP standard and all its subtleties. But the first thing to come in mind is
|
||
|
"how to handle routing"?
|
||
|
|
||
|
One of the advantage of using a language with some types flexibility is to use
|
||
|
the types as a high level specification.
|
||
|
|
||
|
** Routing
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
data ShaiHuludApp = Routes -> Application
|
||
|
#+END_SRC
|
||
|
|
||
|
That's easy, provided a "Routes" representation we should be able to "generate"
|
||
|
a WAI Application. Now how should we represent a set of =Routes=?
|
||
|
|
||
|
We should split them by:
|
||
|
|
||
|
- HTTP Verb: =GET=, =POST=, =PUT=, =DELETE=, =HEAD=, =OPTIONS=, ...
|
||
|
- Path: =/=, =/users/:userID= ...
|
||
|
- Content-Type: =application/json=, =text/plain=, =text/html=, =text/css=,
|
||
|
=application/javascript=...
|
||
|
|
||
|
Hmmm....
|
||
|
|
||
|
So it is immediately very difficult. And these are just the basic requirement,
|
||
|
what about all subtelties about Standardized Headers (CORS, ETags, ...), Custom
|
||
|
Headers...
|
||
|
|
||
|
** Is that FP compatible?
|
||
|
|
||
|
As a functional programmer, and more generally as a scientis, math lover I
|
||
|
immediately dislike that profusion of details with a lot of ambiguities.
|
||
|
|
||
|
For example, REST is still ambiguous, should you use POST / PUT to update?
|
||
|
Should you put a parameter in:
|
||
|
- part of the path like =/user/foo=
|
||
|
- in the query param of the URI =/user?userID=foo=
|
||
|
- in the body? Then what parser should we use? FORM param, JSON, XML?
|
||
|
- in the headers?
|
||
|
- Why not as an anchor? =/user#foo
|
||
|
- How should I provide a parameter that is a list? A set? A Hash-map? Something more complex?
|
||
|
|
||
|
The problem of web programming come from the tooling. Browsers and HTTP evolved
|
||
|
together and some feature existed in browser before people had really the time
|
||
|
to think corectly about them.
|
||
|
|
||
|
That's called real-life-production-world. And it sucks! For a better critique of
|
||
|
web standards you should really read [the chapter «A Long digression into how
|
||
|
standards are made» in Dive into
|
||
|
HTML5](http://diveintohtml5.info/past.html#history-of-the-img-element).
|
||
|
|
||
|
So how could we get back our laws, our composability? Our maths and proofs?
|
||
|
|
||
|
We have a lot of choices, but unfortunately, all the tooling evolved around the
|
||
|
existing standards. So for example, using GET will be cached correctly while
|
||
|
POST won't. And a lot of these details.
|
||
|
|
||
|
*** FP Compatible Web Programming?
|
||
|
|
||
|
Let's re-invent web programming with all we know today.
|
||
|
|
||
|
First, one recent trends has changed a lot of things.
|
||
|
Now a web application is splitted between a frontend and backend development.
|
||
|
|
||
|
The frontend development is about writing a complete application in a browser.
|
||
|
Not just a webpage. The difference between the two notions is blurred.
|
||
|
|
||
|
Once consequence is that now, backend application should only present Web API
|
||
|
and should never send rendering informations. Only datas. So this is a
|
||
|
simplification, the backend should simply expose "procedures", the only things
|
||
|
to think about are the size of the parameter to send and the size of the
|
||
|
response. As every of these objects will go through the wire.
|
||
|
|
||
|
But there are interresting rules:
|
||
|
|
||
|
- =GET= for read only functions
|
||
|
- =POST= generic read/write functions
|
||
|
- =PUT= idempotent read/write functions
|
||
|
- =DELETE= like =PUT= but can delete things
|
||
|
|
||
|
But there are also HTTP code with so many different semantics.
|
||
|
|
||
|
- =1xx=: technical detail
|
||
|
- =2xx=: Successful
|
||
|
- =3xx=: Redirection
|
||
|
- =4xx=: Client Error
|
||
|
- =5xx=: Server Error
|
||
|
|
||
|
So there are some similarities with the HTTP 1.1 reference and the control on
|
||
|
functions we try to achieve with Haskell.
|
||
|
|
||
|
One thing I'd like to imagine is simply that a Web API should simply be like a
|
||
|
library. We could simplify everything _a lot_ by removig most accidental
|
||
|
complexity.
|
||
|
|
||
|
If we consider a web application to be split between a frontend application and
|
||
|
a backend application it changes a lot of things. For example, we could mostly
|
||
|
get rid of urls, we can consider to use the backend as a way to expose
|
||
|
procedures.
|
||
|
|
||
|
Let's for example decide to use only POST, and send parameters only in the body.
|
||
|
|
||
|
In Haskell we could write:
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
foo :: IO X -- ⇒ POST on /foo
|
||
|
bar :: A -> IO X -- ⇒ POST on /foo with a body containing an A
|
||
|
#+END_SRC
|
||
|
|
||
|
And that's it.
|
||
|
|
||
|
* Appendix
|
||
|
** Haskell Fragmentation vs Di
|
||
|
|
||
|
There are many other prelude, one of my personal problem with Haskell is fragmentation.
|
||
|
|
||
|
Someone could see "diversity" another one "fragmentation".
|
||
|
|
||
|
Diversity is perceived as positive while fragmentation isn't.
|
||
|
|
||
|
So is diversity imply necessarily fragmentation?
|
||
|
Could we cure fragmentation by making it hard for people to compete?
|
||
|
|
||
|
I don't think so. I believe we could have the best of both world.
|
||
|
|
||
|
Then fragmentation occurs. And fragmentation is bad, because if you have an
|
||
|
issue with your choice, the number of people that could help you is by nature
|
||
|
reduced.
|
||
|
|
||
|
I would say that there is fragmentation when there is no one obvious choice. But
|
||
|
having an obvious choice for a library for example doesn't really prevent
|
||
|
diversity. Fragmentation:
|
||
|
|
||
|
- NixOS, raw cabal + Linux, stack
|
||
|
- preludes
|
||
|
- editor
|
||
|
- stream library
|
||
|
- orientation of the language "entreprisy ready, production oriented" make
|
||
|
it work being dirty, add dirty choices for research people working in the
|
||
|
language, "research oriented" make it beautiful or don't make it, block
|
||
|
entreprisy people.
|
||
|
|
||
|
** =bracket_=
|
||
|
|
||
|
[^3]: Also if you are
|
||
|
curious and look at its implementation it's quite short and at least for me,
|
||
|
easy to inuit.
|
||
|
|
||
|
#+BEGIN_SRC haskell
|
||
|
bracket :: IO a -- ^ computation to run first (\"acquire resource\")
|
||
|
-> (a -> IO b) -- ^ computation to run last (\"release resource\")
|
||
|
-> (a -> IO c) -- ^ computation to run in-between
|
||
|
-> IO c -- returns the value from the in-between computation
|
||
|
bracket before after thing =
|
||
|
mask $ \restore -> do
|
||
|
a <- before
|
||
|
r <- restore (thing a) `onException` after a
|
||
|
_ <- after a
|
||
|
return r
|
||
|
|
||
|
bracket_ :: IO a -> IO b -> IO c -> IO c
|
||
|
bracket_ before after thing = bracket before (const after) (const thing)
|
||
|
#+END_SRC
|
||
|
|
||
|
Very nice
|