Tools Yesod Lead image: Photo by Aidas Ciziunas, Unsplash.com
Photo by Aidas Ciziunas, Unsplash.com
 

Haskell framework for the web

Speaking in Tongues

The Yesod web framework includes a Haskell compiler on top of the standard web technologies, such as HTML, JavaScript, and CSS, with aggressive optimization and function checks for correctness. By Angela Minosi

Frameworks for the web are a dime a dozen. Most are based on the PHP and JavaScript programming languages, whereas others use Python, Ruby, Go, or other such languages. When a developer uses WordPress [1], Drupal [2], or Joomla [3], generally they only have to unpack the core on the server and then add other modules. Installing security updates conscientiously and in good time counteracts ubiquitous malware threats.

Quite a few reasons speak to the use of exotic frameworks such as the Haskell-based Yesod [4] [5]. It is not just that Yesod is less widespread, it is also that the security built into the compiler (Haskell is type safe) makes attacks time-consuming or impossible, and SQL and JavaScript injections unlikely. Additionally, the Glasgow Haskell Compiler (GHC) [6], version 1.0 of which has existed since April 1990, is based on the fast C compiler and processes requests to the website far more efficiently than many other programming languages. Because Haskell is a functional programming language, it facilitates other tasks, such as multithreading.

Developers mainly ask themselves one question: How do I get started with the framework? Haskell is considered difficult to learn, because it is strongly orientated on mathematical logic and uses many abstract concepts like monads and category theory. A framework like Yesod helps developers make the transition to Haskell and levels a still steep learning curve, thanks to pre-built components that promise quick results. In this article, I will help you take your first steps with Yesod.

Installation

Stack [7] is available to help you install the framework and set up a software project. The process differs from one operating system to the next, but this article describes its use on Fedora 25, which first needs a couple of packages from the unofficial repository [8] to make Stack operational (Listing 1). To start a new Yesod project, just install the Haskell compiler locally:

cd
stack setup

Listing 1: Stack on Fedora 25

dnf install perl make automake gcc gmp-devel libffi zlib zlib-devel xz tar git gnupg dnf-plugins-core
dnf copr enable petersen/stack
dnf install stack

After the install, the Haskell compiler is located under ~/.stack/programs/x86_64-linux/<GHC version>/bin. Additional packages that a Yesod project might require can be installed with the simple stack install <package name>, as needed.

Home Base

Once you have overcome the first hurdle, the real work begins. Yesod supports various popular databases, including MongoDB, SQLite, and, to some extent, Redis. Here, I describe the integration of PostgreSQL [9] and MySQL [10]. To begin, create a new project by entering the command:

stack new <project name> yesod-<database>

Replace <database> with a valid value, which you can take from a table online [11] (e.g., postgres or mysql).

If you want the web application to load data into a MySQL database, the database must be running in the background beforehand (Listing 2). Next, change to the project directory generated by Stack and edit the config/settings.yml file. Under the database: heading, adjust the entries for the database user, host, and the name of the database, because otherwise the Haskell compiler will not compile the project.

Listing 2: Setting Up the MySQL DB

mysql -u root -h localhost -p
mysql> CREATE DATABASE <mydb>;
mysql> CREATE USER '<user>'@'<localhost>' IDENTIFIED BY '<password>';
mysql> GRANT ALL PRIVILEGES ON <mydb.*> TO '<user>'@'<hostname>' IDENTIFIED BY '<password>';

If you want to use a PostgreSQL database for your project, start with the steps in Listing 3. If you do not have high demands on the database, your best database option is SQLite [12], which is intended for a single user only. Yesod creates the database in the project folder, which means that the database is ready for use immediately.

Listing 3: Setting Up the PostgreSQL DB

systemctl start <database>
psql -U postgres -c "CREATE EXTENSION adminpack;"
postgres=# CREATE USER '<user>' WITH PASSWORD '<password>';
postgres=# CREATE DATABASE <myproject>;
postgres=# GRANT ALL PRIVILEGES ON DATABASE <myproject> TO '<user>';
postgres=# \q

First Contact

Stack writes the packages required for compiling the project to the stack.yaml file. The tool automatically selects the latest snapshot of the Haskell compiler, which can be changed using the entry next to resolver:; you can choose a snapshot from the Stackage Snapshots page [13]. Alternatively, you can specify the version number of a globally installed Haskell compiler you want to use.

Once you have processed the stack.yaml file, you can download the required packages with stack build and install them locally. Next, launch the project in developer mode with the

stack exec --yesod devel

command. Provided you have not changed the domain name, you can call the associated web page in the browser of your choice with http://localhost:3000 (Figure 1).

Developer mode comes with an integrated web server. Users reach the website by typing https://localhost:3000 in their browsers.
Figure 1: Developer mode comes with an integrated web server. Users reach the website by typing https://localhost:3000 in their browsers.

For a first demonstration, you just need to create a single file called contact.hs and fill it with the Listing 4 content, which is based on an example from Yesod inventor Michael Snoyman [14] and generates a simple contact form. Being just one file removes the need for project initialization with Stack. The simple web application then calls a simple Stack command:

stack runghc /<Path/to>/contact.hs

Listing 4: contact.hs

01 -- Quasi quotes in curly brackets
02 {-# LANGUAGE ViewPatterns #-}
03 {-# LANGUAGE MultiParamTypeClasses #-}
04 {-# LANGUAGE OverloadedStrings     #-}
05 {-# LANGUAGE TypeFamilies          #-}
06 {-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
07 -- import libraries
08 import Yesod
09 import Data.Text
10 import Control.Applicative
11 import Yesod.Form
12 - set the web server port Warp to 3000
13 main :: IO ()
14 main = warp 3000 FormApp
15 data FormApp = FormApp
16 instance Yesod FormApp
17 instance RenderMessage FormApp FormMessage where
18     renderMessage _ _ = defaultFormMessage
19 - the "FormApp" web application only provides one route
20 mkYesod "FormApp" [parseRoutes|
21 /contact  ContactR GET |]
22 - an auxiliary function integrates the form widget into a web page
23 page enctype widget res = defaultLayout $ do
24 -- set the title of the website
25         setTitle "Contact"
26         [whamlet|
27                 <p>Result: #{show res}
28                 <form enctype=#{enctype}>
29                         ^{widget}
30                         <button>Send
31     |]
32 - HTML input field data types
33 data ContactForm = ContactForm {
34      contactName ::  Text
35     ,contactEmail :: Text
36     ,contactText :: Textarea
37 } deriving Show
38 - function generates a form of MForm type
39 createForm ::  Html -> MForm Handler (FormResult ContactForm, Widget)
40 createForm extra = do
41 -- mreq = required -> required field
42         (nameRes,nameView) <- mreq textField "Name" Nothing
43         (emailRes,mailView) <- mreq emailField  "Email" Nothing
44         (textRes,textView) <- mreq textareaField "Text" Nothing
45         let contactRes = ContactForm <$> nameRes <*> emailRes <*> textRes
46             widget = do
47             toWidget
48 -- integrate Lucius stylesheet
49                 [lucius|
50                     @bColor: mediumseagreen;
51                     ##{fvId nameView}, ##{fvId mailView}, ##{fvId textView}{
52                         border: 3px solid #{bColor};
53                     }
54                 |]
55             [whamlet|
56                 #{extra}
57           <h1>Contact Form!
58                 <p>Name: ^{fvInput nameView}
59                 <p>Email: ^{fvInput mailView}
60                 <p>Text: ^{fvInput textView}
61             |]
62         return (contactRes,widget)
63 getContactR :: Handler Html
64 getContactR = do
65    ((res,widget), enctype) <- runFormGet $ createForm
66 page enctype widget res

The code in Listing 4 asks site visitors for their names and email addresses and prompts them to leave a message (Figure 2). When you access the web page, the getContactR function (lines 63 and 64) generates the form, and clicking on Send outputs the previously submitted data.

The contact form can be implemented in a single page and is not a complex project.
Figure 2: The contact form can be implemented in a single page and is not a complex project.

Yesod Ecosystem

The listings that follow are excerpts from the project folder, which Stack generates as soon as you call:

stack new <My project>

The original project essentially comprises the pages shown in a login and link to Yesod documents (Figure 1), the possibility to upload files (Figure 4), and built-in support (Figure 5). Also, the home page includes a brief introduction to the framework.

In the last part of the homepage, users either leave a comment or learn more about testing software under Yesod.
Figure 5: In the last part of the homepage, users either leave a comment or learn more about testing software under Yesod.

The login overrides the sample project with the option to register with Google and Yahoo (Figure 3) and sets the maximum number of uploadable files to one (Figure 4). For a quick start in Yesod, you still need to know which information belongs where. Table 1 provides an overview of the most important files.

Tabelle 1: <My project> Folder

Folder/File Name

Content

Application.hs

Initialize application, register handler

Foundation.hs

Configure plugins

<Project name>.cabal

Register extensions, packages, and handler

stack.yaml

Packages, GHC

config/settings.yml

Global variables

config/favicon.ico

Icon

config/models

Database schema

config/routes

Routes

Handler/handler.hs

Handler

static/

CSS, JavaScript, images, fonts

templates/

Templates in Hamlet, Julius, Cassius, or Lucius

This login window appears when you click Login on the homepage of the project.
Figure 3: This login window appears when you click Login on the homepage of the project.
A variable now indicates the maximum number of uploadable files in the form of the slightly altered website.
Figure 4: A variable now indicates the maximum number of uploadable files in the form of the slightly altered website.

Now you can edit the files and define an individual icon for the URL bar; you can replace the config/favicon.ico file with an original composition of the same name. You can modify the database schema in the config/model file, as necessary. Changes affect the current system once you save them – you do not need to restart the web application – and applies to changes to other files in the project.

In the config/routes file, you determine the routes (i.e., the URLs) that users use to reach the individual websites. To do so, you can store POST, GET, and other methods for each route in the same file.

Yesod also requires one handler per route. The handler determines what will happen as soon as you call a route. It is best to create a new handler in the handler folder, where you should make sure to use the typical Haskell .hs file suffix. Last but not least, you need to define the name of the domain in which the Yesod framework runs in the config/settings.yml file below the AppRoot keyword.

The static subfolder includes JavaScript files, style sheets, fonts, and images. It is worth mentioning in this context that the project includes the widespread Bootstrap CSS framework [15]. The static folder only contains pure CSS or JavaScript files here, because Yesod has its own template language, and the files created with it are usually in the templates directory.

The <My project>/test subfolder contains sample code that you use to put the web application through its paces. Additionally, several interesting files reside in the main folder of the project. Yesod initializes global variables or data types in the Application.hs (Listing 5) file, and the handler uses the getYesod function to access their values (Listing 6).

Listing 5: <My project>/Application.hs

01 [...]
02 import Model.MySettings
03 [...]
04 makeFoundation :: AppSettings -> IO App
05 makeFoundation appSettings = do
06     [...]
07 - maximum number of uploadable files
08     let maxFiles = 5
09     mysettings' <- newSettings maxFiles
10 -- make "myMax" version available globally
11         let mkFoundation appConnPool = App mysettings'
12 [...]

Listing 6: <My project>/handler/Home.hs (Extract)

01 [...]
02 module Handler.Home where
03 [...]
04 -- handler that created the www.domainname page
05 getHomeR :: Handler Html
06 getHomeR = do
07 [...]
08 - access to global variables with getYesod
09         (App{mySettings=s}) <- getYesod
10      let (M.MySettings {M.maxFiles=m }) = s
11 [...]

For example, if a website user can upload a maximum of five images per ticket, you would import a separate file (Listing 7), in which you have set a data type and a function that initializes the variable.

Listing 7: <My project>/Model/MySettings.hs

01 module Model.MySettings
02     (
03           MySettings(..)
04         , newSettings
05     )where
06 import [...]
07 -- type synonym
08 type Max = Int
09 --Datentyp MySettings
10 data MySettings = MySettings
11     { myMax         :: Max }
12 --this function determines the maximum number of uploadable files
13 newSettings num = do
14   return MySettings { myMax = num }

You then declare a local variable in the makeFoundation function in Application.hs (Listing 5): The variable contains the maximum number of uploadable files and passes this variable in to the imported function, which initializes the variable. Additionally, you need to define which data types belong to the web application named App (Listing 8).

Listing 8: <My project>/Foundation.hs (Extract)

01 [...]
02 import Model.MySettings
03 [...]
04 --Name the of the web application = App
05 data App = App
06 - In the app, only the variables of the data type MySettings appear.
07 - Under Java developers could compare MySettings with a class,
08     --which has the myApp variable. This is from MySettings type.
09     {mySettings    :: MySettings }
10 [...]

To Be or Not To Be

Shakespeare provides the name of the template language that Yesod uses to generate HTML files from a template: Hamlet. If you know HTML, you will have no trouble familiarizing yourself with Hamlet, which supports if-then queries, case distinctions (using case and Maybe), and loops.

If necessary, you can overwrite the settings in the global style sheet with the use of two style sheet languages: Cassius and Lucius. Whereas Lucius, like CSS, requires brackets when defining individual selectors and a semicolon at the end of a statement, Cassius gets by without this overhead. However, with Cassius, you do need to indent the definition of a selector.

Unlike CSS, Lucius and Cassius can define variables, saving lines of code. Using Julius, you can create JavaScript files that access the existing variables in the project, such as route names. You can statically integrate existing CSS style sheets or JavaScript files without having to convert them to another format. In Listing 9, Yesod loads the static/css/styling.css style sheet when the user calls a route of their choice.

Listing 9: <My project>/Foundation.hs (Extract)

01 [...]
02 - defines the default layout, which applies to each page of a given domain
03 pc <- widgetToPageContent $ do
04       addStylesheet $ StaticR css_styling_css
05 - the standard layout tempate is in the "default-layout.hamlet" file
06       $(widgetFile "default-layout")
07     withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
08 [...]

However, it makes sense to use the template languages for new files. For example, if you want the handler that implements the homepage to integrate both HTML code in a Hamlet file and an appropriate style sheet and Julius file on top, you should ensure that the names of these files are all the same (e.g., homepage.hamlet, homepage.julius, and homepage.lucius). In this way, you only need to include a file in the handler once and then reference homepage (Listing 10) in the handler. Yesod then automatically loads the templates.

Listing 10: <My project>/handler/Home.hs (Extract)

01 [...]
02 module Handler.Home where
03 [...]
04 - handler that generates the home page of a domain
05 - In the homepage.hamlet file you will find the template for the home page
06 getHomeR :: Handler Html
07 getHomeR = defaultLayout $(widgetFile "homepage")
08 [...]

Guaranteeing Security

If a user contacts the webmaster via a contact form, you can generate the necessary forms under Yesod without having to write multiple lines of code. Two different methods give you the same results. If you just want to get started, type A forms are suitable. You don't need an additional Hamlet file for them; you just need to specify which HTML fields the form will comprise, as in the contactAForm function (Listing 11).

Listing 11: contact-v2.hs

01 [...]
02 data ContactForm = ContactForm {
03      contactName ::  Text
04     ,contactEmail :: Text
05     ,contactText :: Textarea
06 } deriving Show
07
08 - generates a type A form
09 contactAForm :: AForm Handler ContactForm
10 contactAForm = ContactForm
11     <$> areq textField "Name"  Nothing
12     <*> areq textField  "Email"   (Just "info@example.com")
13     <*> areq textareaField "Text"  Nothing
14
15 contactForm :: Html -> MForm Handler (FormResult ContactForm, Widget)
16 contactForm = renderBootstrap2 contactAForm
17
18 getContactR = do
19    (widget, enctype) <- generateFormPost $  contactForm
20    page enctype widget
21 [...]

You can define the appearance of monadic M forms, mentioned at the top of the article, yourself (Figure 2), but this does mean you add the previously defined HTML input elements to the Hamlet section. You also add classes or IDs to the Hamlet files that you previously defined in Lucius or Cassius (Listing 4).

Yesod guarantees security with the use of security tokens, which are stored in a hidden HTML input field. These tokens prevent cross-site request forgery attacks. You only need to enter the #{extra} variable in the Hamlet section for M forms. Yesod automatically adds the extra variable for A forms.

Clever Compiler

When user input reaches the server, Yesod checks it for JavaScript injections by testing the data with JavaScript commands, such as <script></script>, and disarms the threat. For example, Yesod replaces < with &lt;. Following the same principle, Yesod protects the database against SQL injections [16].

The Haskell compiler checks the function type when compiling, which rules out type violations. It also checks the templates created in a template language from the Shakespearean family for syntax errors, ruling out any surprises during operation.

Cookies

Yesod is a RESTful application, but sometimes the framework has to save session data when interacting with clients (e.g., when using a shopping cart). The clientsession package uses encryption and signatures to provide data in cookies. On the one hand, this ensures that the user does not manipulate the data; on the other hand, the signature prevents man-in-the-middle attacks.

The developer determines how long a cookie is valid. To use cookies, use the Foundation.hs file to define where Yesod stores the cookie on the server. Listing 12 shows a session cookie. Firefox accepts such cookies, even in private mode (Figure 6), so you do not need to do without them. Cookies also hide sensitive data related to the URL.

Listing 12: <My project>/Foundation.hs (Extract)

01 [...]
02 instance Yesod App where
03     makeSessionBackend _ = Just <$> defaultClientSessionBackend
04         -- timeout in minutes
05         120
06         "<Path/to/Cookie>.aes"
Firefox displays the encrypted content of a cookie. The cookies generated by Yesod are usually valid for a period of two hours.
Figure 6: Firefox displays the encrypted content of a cookie. The cookies generated by Yesod are usually valid for a period of two hours.

Recycling Code

Modularity means, among other things, reusability of code in other projects, which can be implemented under Yesod using subsites. These subsites are standalone programs (e.g., a chat application that occurs in several software projects). You only need to copy the folder that contains the subsite to the new project.

When implementing subsites, it should be noted that, although Snoyman has some examples in his book [14], not much useful information is available on the web; therefore, you need to teach yourself or gather information from different sources.

Another example of modularity with Yesod is Haskell programs or Haskell modules. For example, a game written in Haskell can be imported into a Yesod project, which the user accesses from the browser.

Authentication Required

To protect the user accounts, Yesod provides libraries, which you use to implement the login procedure. The library includes Yesod.Auth.OpenId [17], which provides a way for users to log in to an existing Google+ or Yahoo account on the website, without needing the webmaster to manage the passwords (Listings 13 and 14). To implement this authentication, the Foundation.hs file has to import the above-mentioned library. Enable the plugin and also specify which routes do not need authentication or a login (Listing 15).

Listing 13: <My project>/GoogleEmail.hs

01 module googleemail where
02 import Data.Text
03
04 clientId :: Text
05 clientId = "<myid>"
06
07 clientSecret :: Text
08 clientSecret = "<mysecret>"

Listing 14: <My project>/Foundation.hs (Extract)

01 [...]
02 import Yesod.Auth.OpenId
03 import Yesod.Auth.GoogleEmail2
04 import Googleemail
05 [...]
06 instance YesodAuth App where
07     authPlugins m =
08         [ authGoogleEmail clientId clientSecret ]
09
10  [...]
11 - Default value: Authorized
12  isAuthorized _ _ = return Authorized
13  [...]

Listing 15: <My project>/Foundation.hs (Extract)

01 [...]
02 instance YesodAuth App where
03     loginHandler = defaultLayout $ do
04       [whamlet|
05         <form method="get" action=@{AuthR forwardUrl} >
06            <input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id">
07         <button class="btn-openid">
08            <span>Login via Google
09       |]
10  [...]

Sometimes it is also necessary to add libraries to the <Project name>.cabal project file for Yesod to compile the project, which the framework points out at compile time.

By overwriting the login handler in the Foundation.hs file (Listing 15), you can design the login page to suit your needs. Simple HTML code is all you need in Yesod to implement the login handler.

Conclusions

The performance of Haskell, on which Yesod is based, is good enough to form the core of the Facebook Sigma system. Among other things, Facebook uses Haskell for spam prevention, and the production system used there handles approximately one million requests per second [18].

According to the project's own metrics [19], with a large Amazon EC2 instance, the Haskell-based Yesod is one of the fastest web frameworks. It also offers everything you need to tackle larger projects, because it supports, for example, a number of well-known databases.

Although the Java and PHP programming languages have numerous how-tos for payment systems such as PayPal, helpful hints are rarely given for Yesod or Haskell. Yesod users will search in vain for add-ins, whereas, Drupal and WordPress, for example, offer plugins numbering into five digits.

Additionally, the Haskell base puts your staying power to the test despite numerous simplifications. For example, it is not always easy to decipher error messages from the compiler; nevertheless, of the Haskell-based web frameworks, Yesod is one of the most advanced, probably because it comes with its own template languages and numerous libraries. In addition to the advantages mentioned initially, Yesod provides inquisitive developers with an incentive to plumb the depths of practical examples in the Haskell world.