her.esy.fun/src/drafts/XXXX-raw-haskell-web-app/index.org

505 lines
16 KiB
Org Mode
Raw Normal View History

2019-07-20 19:58:48 +00:00
#+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