Yesod Web Framework Book 2 | OpenTopic | TOC Contents - - PDF document
Yesod Web Framework Book 2 | OpenTopic | TOC Contents - - PDF document
Yesod Web Framework Book 2 | OpenTopic | TOC Contents Basics......................................................................................................................................5
2 | OpenTopic | TOC
Contents
Basics......................................................................................................................................5
Introduction.............................................................................................................................................. 5 Type Safety.................................................................................................................................. 5 Concise.........................................................................................................................................5 Performance................................................................................................................................. 5 Modular........................................................................................................................................ 6 A solid foundation........................................................................................................................6 Introduction to Haskell.............................................................................................................................6 Language Pragmas....................................................................................................................... 6 Quasi-quotation............................................................................................................................ 6 Type families................................................................................................................................6 Basics....................................................................................................................................................... 6 Getting Yesod...............................................................................................................................7 Library versus Framework........................................................................................................... 7 Hello World..................................................................................................................................7 Routing.........................................................................................................................................8 Handler function...........................................................................................................................8 The Foundation............................................................................................................................ 8 Running........................................................................................................................................ 9 Resources and type-safe URLs.................................................................................................... 9 Development server....................................................................................................................10 Summary.................................................................................................................................... 10 Templates............................................................................................................................................... 10 Type Safety................................................................................................................................ 11 Basic syntax................................................................................................................................12 Tags............................................................................................................................................ 14 Variables.....................................................................................................................................15 Function Application..................................................................................................................15 Hamlet Control Structures..........................................................................................................16 Comments...................................................................................................................................17 Templates in External Files........................................................................................................17 Types.......................................................................................................................................... 19 Lucius.........................................................................................................................................19 Hamlet Syntax............................................................................................................................20 Summary.................................................................................................................................... 20 Widgets...................................................................................................................................................20 What's in a Widget?....................................................................................................................21 Building Widgets........................................................................................................................21 Yesod Typeclass.....................................................................................................................................22 Rendering and Parsing URLs.....................................................................................................23 defaultLayout............................................................................................................................. 25 Custom error pages.....................................................................................................................27 Summary.................................................................................................................................... 28 Routing and Handlers.............................................................................................................................28 Route Syntax.............................................................................................................................. 28 Dispatch......................................................................................................................................31 The GHandler Monad.................................................................................................................32 Summary.................................................................................................................................... 34 Basic Forms............................................................................................................................................35 Random bananas........................................................................................................................ 35 Summary.................................................................................................................................... 42
OpenTopic | TOC | 3 Sessions.................................................................................................................................................. 42 Clientsession...............................................................................................................................43 Controlling sessions................................................................................................................... 43 Session Operations..................................................................................................................... 44 Messages.................................................................................................................................... 44 Ultimate Destination.................................................................................................................. 44 Summary.................................................................................................................................... 45 Persistent................................................................................................................................................ 45 Solving the boundary issue........................................................................................................ 45 Migrations.................................................................................................................................. 48 Attributes....................................................................................................................................49 Associated Types........................................................................................................................50 Relations.....................................................................................................................................53 Custom Fields.............................................................................................................................54 Persistent: Raw SQL.................................................................................................................. 54 FIXME Outline.......................................................................................................................... 55
Advanced............................................................................................................................. 56
RESTful Content....................................................................................................................................56 Request methods........................................................................................................................ 56 Representations.......................................................................................................................... 56 Other request headers.................................................................................................................59 Stateless......................................................................................................................................59 Summary.................................................................................................................................... 59 Authentication and Authorization.......................................................................................................... 60 Scaffolding and the Site Template......................................................................................................... 60 Advanced Forms.................................................................................................................................... 60 Testing....................................................................................................................................................60 Sending Email........................................................................................................................................ 60 Deploying your Webapp........................................................................................................................ 60 Warp...........................................................................................................................................61 FastCGI...................................................................................................................................... 62 Desktop.......................................................................................................................................63 CGI on Apache...........................................................................................................................63 FastCGI on lighttpd....................................................................................................................64 CGI on lighttpd...........................................................................................................................64 Internationalization.................................................................................................................................64 Creating a Subsite...................................................................................................................................64 Hello World................................................................................................................................65 Web Client Code.................................................................................................................................... 66 JSON Web Service.....................................................................................................................66 Low Level Tricks................................................................................................................................... 68
Appendices...........................................................................................................................69
Enumerator Package...............................................................................................................................69 Iteratees...................................................................................................................................... 69 Enumerators............................................................................................................................... 73 Enumeratees............................................................................................................................... 77 Web Application Interface..................................................................................................................... 80 The Interface.............................................................................................................................. 81 Hello World................................................................................................................................82 Middleware.................................................................................................................................82 Blog posts that should be chapters......................................................................................................... 83 Frequently Asked Questions.................................................................................................................. 83 Why does the scaffolded site use cassiusFileDebug and juliusFileDebug, but does not use hamletFileDebug? 83 Migration Guide: 0.6 to 0.7....................................................................................................................84 No Warnings.............................................................................................................................. 84 Install yesod................................................................................................................................84
4 | OpenTopic | TOC Update cabal file.........................................................................................................................84 Update your Hamlet templates...................................................................................................84 cabal install.................................................................................................................................84 Show, Read and Eq for Html..................................................................................................... 85 MonadInvertIO to MonadPeelIO............................................................................................... 85 urlRenderOverride......................................................................................................................85 runDB: need liftIOHandler.........................................................................................................85 mime-mail Part constructor........................................................................................................85 Feed, formerly known as AtomFeed..........................................................................................86 fileLookupDir no more...............................................................................................................86 MultiParamTypeClasses.............................................................................................................86 String to ByteString....................................................................................................................86 devel-server.hs............................................................................................................................86 lift, not liftHandler......................................................................................................................86 Migration Guide: 0.7 to 0.8....................................................................................................................86 No Warnings.............................................................................................................................. 87 Install yesod................................................................................................................................87 Update cabal file.........................................................................................................................87 cabal install.................................................................................................................................87 persistent-template..................................................................................................................... 87 Language Extensions..................................................................................................................87 External File............................................................................................................................... 87 toHtml.........................................................................................................................................88 MonadPeelIO to MonadControlIO.............................................................................................88 String to Text..............................................................................................................................88 Persist Keys................................................................................................................................88 Explicit Type Signatures............................................................................................................ 88
Examples..............................................................................................................................89
Example: Blog........................................................................................................................................89 Example: Ajax........................................................................................................................................91 Example: Form.......................................................................................................................................93 Example: Widgets.................................................................................................................................. 95 Example: Generalized Hamlet............................................................................................................... 96 Example: Pretty YAML......................................................................................................................... 97 Example: Internationalization................................................................................................................ 98
OpenTopic | Basics | 5
Basics
Introduction
Since web programming began, people have been trying to make the development process a more pleasant one. As a community, we have continually pushed new techniques to try and solve some of the lingering difficulties of security threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, Javascript) necessary to create a powerful web application, and more. Yesod attempts to ease the web development process by playing to the strengths of the Haskell programming
- language. Haskell's strong compile-time guarantees of correctness not only encompass types; referential transparency
ensures that we don't have any unintended side effects. Pattern matching on algebraic data types can help guarantee we've accounted for every possible case. By building upon Haskell, entire classes of bugs disappear. Unfortunately, using Haskell isn't enough. The web, by its very nature, is not type safe. Even the simplest case of distinguishing between an integer and string is impossible: all data on the web is transferred as raw bytes, evading
- ur best efforts at type safety. Every app writer is left with the task of validating all input. I call this problem the
boundary issue: as much as your application is type safe on the inside, every boundary with the outside world still needs to be sanitized.
Type Safety
This is where Yesod comes in. By using high-level declarative techniques, you can specify the exact input types you are expecting. And the process works the other way as well: using a process of type-safe URLs, you can make sure that the data you send out is also guaranteed to be well formed. The boundary issue is not just a problem when dealing with the client: the same problem exists when persisting and loading data. Once again, Yesod saves you on the boundary by performing the marshaling of data for you. You can specify your entities in a high-level definition and remain blissfully ignorant of the details.
Concise
We all know that there is a lot of boilerplate coding involved in web applications. Wherever possible, Yesod tries to use Haskell's features to save your fingers the work:
- The forms library reduces the amount of code used for common cases by leveraging the Applicative type class.
- Routes are declared in a very terse format, without sacrificing type safety.
- Serializing your data to and from a database is handled automatically via code generation.
In Yesod, we have two kinds of code generation. To get your project started, we provide a scaffolding tool to set up your file and folder structure. However, most code generation is done at compile time via meta programming. This means your generated code will never get stale, as a simple library upgrade will bring all your generated code up-to- date. But for those who like to stay in control, and know exactly what their code is doing, you can always run closer to the compiler and write all your code yourself.
Performance
Haskell's main compiler, the GHC, has amazing performance characteristics, and is improving all the time. This choice of language by itself gives Yesod a large performance advantage over other offerings. But that's not enough: we need an architecture designed for performance. Our approach to templates is one example: by allowing HTML, CSS and JavaScript to be analyzed at compile time, Yesod both avoids costly disk I/O at runtime and can optimize the rendering of this code. But the architectural decisions go deeper: we use advanced techniques such as enumerators and builders in the underlying libraries to make
6 | OpenTopic | Basics sure our code runs in constant memory, without exhausting precious file handles and other resources. By offering high-level abstractions, you can get highly compressed and properly cached CSS and JavaScript. Yesod's flagship web server, Warp, is the fastest Haskell web server around. When these two pieces of technology are combined, it produces one of the fastest web application deployment solutions available.
Modular
Yesod has spawned the creation of dozens of packages, most of which are usable in a context outside of Yesod itself. One of the goals of the project is to contribute back to the community as much as possible; as such, even if you are not planning on using Yesod in your next project, a large portion of this book may still be relevant for your needs. Of course, these libraries have all been designed to integrate well together. Using the Yesod Framework should give you a strong feeling of consistency throughout the various APIs.
A solid foundation
I remember once seeing a PHP framework advertising support for UTF-8. In the Haskell world, we usually have the
- pposite problem: there are a number of packages providing powerful and well-designed support for the problem.
The Haskell community is constantly pushing the boundaries finding the cleanest, most efficient solutions for each challenge. The downside of such a powerful ecosystem is the complexity of choice. By using Yesod, you will already have most
- f the tools chosen for you, and you can be guaranteed they work together. Of course, you always have the option of
pulling in your own solution. As a real-life example, Yesod and Hamlet (the default templating language) use blaze-builder for textual content generation. This choice was made because blaze provides the fastest interface for generating UTF-8 data. Anyone who wants to use one of the other great libraries out there, such as text, should have no problem dropping it in.
Introduction to Haskell
Haskell is a powerful, fast, type-safe, functional programming language. This book takes as an assumption that you are already familiar with most of the basics of Haskell. There are two wonderful books for learning Haskell, both of which are available for reading online:
- Learn You a Haskell for Great Good!
- Real World Haskell
Yesod relies on a few features in Haskell that most introductory tutorials do not cover. Though you will rarely need to understand how these work, It's always best to start off with a good appreciation for what your tools are doing. These chapter will cover those features.
Language Pragmas
FIXME
Quasi-quotation
FIXME
Type families
FIXME
Basics
OpenTopic | Basics | 7
Getting Yesod
The rest of this book will assume you have Yesod installed. You'll need to:
- Install the Haskell Platform
- Run cabal update
- Run cabal install alex happy
- Run cabal install yesod
- In general, you should follow the instruction at Yesod in 5 minutes.
Note: It is generally recommended to create new projects using the Yesod scaffolder tool by running yesod init. This book will mostly do things manually for instructive purposes, but for real world projects, even I use the scaffolder to get the basic setup correct.
Library versus Framework
I'm going to be a bit bold and say the defining line between a library and a framework is that a framework tells you how to lay out your code into a file/folder structure. You may not agree with this definition, but it's useful to explain how this book will begin. The Yesod Web Framework comes with a tool that automatically generates a full site template with a bunch of bells and whistles. This is the recommended way to get started on a new Yesod application. This added convenience, however, hides away some of the important details going on behind the scenes. So to start off, we're going to be treating Yesod as a library. Having to explicitly write all the code is a good exercise to get started. Later on, we'll introduce the scaffolding tool and describe the standard layout of a Yesod project.
Hello World
Let's get this book started properly: a simple web page that says Hello World:
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes| / HomeR GET |] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout [$hamlet|Hello World!|] main = warpDebug 3000 HelloWorld
- - STOP
advanced: I have purposely left out the type signature of getHomeR in this snippet because it looks scarier than it really is, and because in real life code with the scaffolding tool it would look different anyway. For the curious, the type signature would be: getHomeR :: GHandler HelloWorld HelloWorld RepHtml
8 | OpenTopic | Basics
- - But the scaffolding tool defines an
alias: type Handler = GHandler HelloWorld HelloWorld
- - So the type signature would just be
getHomeR :: Handler RepHtml If you save that code in helloworld.hs and run it with runhaskell helloworld.hs, you'll get a web server running on port 3000. If you point your browser to http://localhost:3000, you'll get the following HTML: <!DOCTYPE html> <html><head><title></title></head><body>Hello World!</body></html>
Routing
Like most modern web frameworks, Yesod follows a front controller pattern. This means that every request to a Yesod application enters at the same point and is routed from there. As a contrast, in systems like PHP and ASP you
- ften times create a number of different files, and the web server automatically directs requests to the relevant file.
Lines 6 through 8 set up this routing system. We see our only resource defined on line 7. We'll give full details of the syntax later, but this line creates a resource named HomeR, which accepts GET requests at the root (/) of our application. Yesod sees this resource declaration, and determines to call the getHomeR handler function whenever it receives a request for HomeR. The function name follows the simple pattern of request method, in lowercase, followed by the resource name.
Handler function
Most of the code you write in Yesod lives in handler functions. This is where you process user input, perform database queries and create responses. In our simple example, we create a response using the defaultLayout function. By default, this is simply an HTML wrapper that creates a doctype, html, head and body tags. As we'll see later, this function can be overridden to do much more. That funny [$hamlet|Hello World!|] is a quasi-quotation. It allows us to embed arbitrary text in our Haskell code, process it with a specific function and have that generate Haskell code, all at compile time. In our case, we feed the string "Hello World!" to the hamlet quasi-quoter. advanced: While quasi quotation is great for small code snippets, and wonderful for making single-file examples, it does not scale well to production levels, since it mixes logic and presentation in the same file. When we discuss templates, you will see how to put your templates in external files. Hamlet is the default HTML templating engine in Yesod. Together with its siblings Cassius and Julius, you can create HTML, CSS and Javascript in a fully type-safe and compile-time-checked manner. We'll see much more about this when we discuss widgets.
The Foundation
The word "HelloWorld" shows up on lines 4, 6, 10 and 15, yet the datatype doesn't seem to actually do anything
- important. In fact, this seemingly irrelevant piece of code is central to how Yesod works. Each Yesod application has
a single datatype, referred to as its foundation. Line 4 of our example defines this simple datatype. Line 6 does something a bit more interesting: it associates the routing rule we define on line 7 with this datatype. Each foundation must be an instance of the Yesod typeclass; we do this on line 10. We'll get into much more detail on the Yesod typeclass and the approot method in the Yesod typeclass chapter.
OpenTopic | Basics | 9 advanced: By the way, the word Yesod (####) means foundation in Hebrew.
Running
Once again we mention HelloWorld in our main function. Our foundation contains all the information we need to route and respond to requests in our application, now we just need to convert it into something that can run. A great function for this in Yesod is warpDebug, which runs the Warp webserver with debug output enabled on the specified port (here, it's 3000). advanced: In addition to warpDebug, Yesod provides the warp function for a no-debug-output server (useful for production), as well as develServer, which does automatic code reloading. The scaffolded site sets up your development and production builds automatically. One of the great features of Yesod is that you aren't tied down to a single deployment strategy. Yesod is built on top
- f the Web Application Interface (WAI), allowing it to run on FastCGI, SCGI, Warp, or even as a desktop application
using the Webkit library. We'll discuss some of these options in the deployment chapter. And at the end of this chapter, we will explain the development server.
Resources and type-safe URLs
In our hello world, we defined just a single resource (HomeR). A web application is usually much more exciting with more than one page on it. Let's take a look:
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data Links = Links mkYesod "Links" [$parseRoutes| / HomeR GET /page1 Page1R GET /page2 Page2R GET |] instance Yesod Links where approot _ = "" getHomeR = defaultLayout [$hamlet|<a href="@{Page1R}">Go to page 1!|] getPage1R = defaultLayout [$hamlet|<a href="@{Page2R}">Go to page 2!|] getPage2R = defaultLayout [$hamlet|<a href="@{HomeR}">Go home!|] main = warpDebug 3000 Links
- - STOP
Overall, this is the same. Our foundation is now Links instead of HelloWorld, and in addition to the HomeR resource, we've added Page1R and Page2R. As such, we've also added two more handler functions: getPage1R and getPage2R. The only truly new feature is inside the hamlet quasi-quotation on lines 15-17. We'll delve into syntax later, but we can see that: <a href="@{Page1R}">Go to page 1! creates a link to the Page1R resource. The important thing to note here is that Page1R is a data constructor. By making each resource a data constructor, we have a feature called type-safe URLs. Instead of splicing together
10 | OpenTopic | Basics strings to create URLs, we simply create a plain old Haskell value. By using at-sign interpolation (@{...}), Yesod automatically renders those values to textual URLs before sending things off to the user.
Development server
One of the advantages interpreted languages have over compiled languages is fast prototyping: you save changes to a file and hit refresh. If we want to make any changes to our Yesod apps above, we'll need to call runhaskell from scratch, which can be a bit tedious. Fortunately, there's a nice solution to this: wai-handler-devel embeds a Haskell interpreter and automatically reloads code changes for you. This can be a great way to develop your Yesod projects, and when you're ready to move to production, you can compile against a more efficient backend. The Yesod site template comes built in with a script to do this for you. This gives you the best of both worlds: rapid prototyping and fast production code. It's a little bit more involved to set up your code to be used by wai-handler-devel, so our examples will just use
- warpDebug. But as a simple example, try saving the following as HelloWorld.hs:
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} module HelloWorld where import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes| / HomeR GET |] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout [$hamlet|Hello World!|] withHelloWorld f = toWaiApp HelloWorld >>= f And then run your code with: wai-handler-devel 3000 HelloWorld withHelloWorld This will run a development server on port 3000, using module HelloWorld and the function withHelloWorld. Try making changes to the HelloWorld.hs file and reloading: they should show up automatically. In order to shut down the server, type "q" and hit enter on the command line.
Summary
Every Yesod application is built around a foundation datatype. We associate some resources with said datatype and define some handler functions, and Yesod handles all of the routing. These resources are also data constructors, which lets us have type-safe URLs. By being built on top of WAI, Yesod applications can run with a number of different backends. warpDebug is an easy way to get started, as it's included with Yesod. For rapid development, wai-handler-devel is a good choice. And when you're ready to move to production, there are high-performance options like Warp and FastCGI.
Templates
Yesod is built upon the hamlet package, which provides three templating systems: Hamlet (HTML), Cassius (CSS) and Julius (Javascript). These systems are all available for use outside the realm of Yesod.
OpenTopic | Basics | 11 advanced: Starting with Yesod 0.8, there is a new CSS template language, Lucius. While Cassius uses white space to imply nesting, Lucius is a superset of CSS itself, and therefore uses braces/semicolons. Both languages are currently fully supported, first-class Yesod citizens. If we end up with a clear winner between them in the future, we might consider deprecating one. In this chapter, we will explore the syntax of these languages, how they can be combined to form complete documents, and where to actually put the content. To start off, our examples will use the same quasi-quotation syntax as the basics chapter, and we will move on from there. advanced: Just a little heads-up: in order to simplify the examples in this chapter, we're going to assume the OverloadedStrings language extension. If you rather not use this extension, you'll need to make some minor modifications, such as replacing: setTitle "Home Page" with setTitle $ string "Home Page" The goal of hamlet syntax is essentially to remove all redundancy from html. By staying closer to html, we can:
- avoid learning arbitrarily new syntax
- appeal to a wider variety of programmers and
- appeal to designers
Just by making white space significant and removing closing tags, we eliminate the main source of invalid html. With that, and some id/class shortcuts, and making attribute quoting optional, we have eliminated the main sources of tedium in html.
Type Safety
One of the biggest features of Yesod is its pervasive type safety. This was the original impetus for the creation
- f Hamlet. As such, all variable interpolations gets checked at compile time. Hamlet supports three forms of
interpolation:
- Any instance of the ToHtml typeclass. This includes String and Html. This is also where Hamlet shows great XSS
(XSS)-attack resilience: whenever interpolating a String, Hamlet automatically escapes all HTML entities. This is called variable interpolation.
- Type-safe URLs. We mentioned in the basics chapter that each URL can be represented as a Haskell value, and
then we use a render function to convert those values into strings. Hamlet allows you to interpolate those type-safe URLs directly. This is called URL interpolation.
- Other Hamlet templates. This is great for creating a chunk of code that will be reused in other templates. This is
called embedding. Cassius provides support for the first two forms of interpolation, but instead of the ToHtml typeclass, there is the ToCss typeclass. Embedding does not make as much sense with CSS. Julius provides support for all three. advanced: Cassius used to provide support for something similar to embedding, called mixins. This was removed due to implementation details regarding the whitespace syntax. We now also have Lucius (described below) that provides these features.
12 | OpenTopic | Basics In order to perform an interpolation, you enter the interpolation character, followed by the variable inside braces. For example, My name is #{name}. The hash is used for variable interpolation, at-sign (@) for URL interpolation, and caret (^) for embedding.
Basic syntax
We have already seen some Hamlet in the basics chapter. Let's have a quick review of our Hello World example:
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes| / HomeR GET |] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout [$hamlet|Hello World!|] main = warpDebug 3000 HelloWorld
- - STOP
Line 13 shows our Hamlet quasi-quotation, surrounded by [$hamlet| and |]. The Haskell compiler knows to parse the internal code, convert it to Haskell, and then compile that. But that's a really boring HTML page: it's just the text "Hello World!" without any tags! Let's see something a little bit more interesting: {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = ""
- - START
getHomeR = defaultLayout [$hamlet| <h1>Hello World! <p>Here are some of my favorite links: <ul> <li> <a href=http://www.yesodweb.com/>Yesod Web Framework Docs <li> <a href=http://www.haskell.org/>Haskell Homepage <p>Thanks for visiting! |]
- - STOP
main = warpDebug 3000 HelloWorld Overall, Hamlet syntax looks fairly similar to HTML. However, there is one important difference: instead of closing tags manually, nesting is determined by indentation level. The goal of Hamlet is to provide a less error-prone, more DRY (DRY) syntax for HTML, without introducing a completely foreign language that developers and designers will need to relearn. Similarly, Cassius is also whitespace-sensitive: {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld
OpenTopic | Basics | 13 mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout $ do [$hamlet| <h1>Hello World! <p>Here are some of my favorite links: <ul> <li> <a href=http://docs.yesodweb.com/>Yesod Web Framework Docs <li> <a href=http://www.haskell.org/>Haskell Homepage <p>Thanks for visiting! |] addCassius
- - START
[$cassius| h1 color: green ul > li:first-child border-left: 5px solid orange |]
- - STOP
main = warpDebug 3000 HelloWorld On the other hand, Julius is mostly a pass-through format, allowing you to write your Javascript however you like: {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout $ do addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/ jquery.min.js" [$hamlet| <h1>Hello World! <p>Here are some of my favorite links: <ul> <li> <a href=http://docs.yesodweb.com/>Yesod Web Framework Docs <li> <a href=http://www.haskell.org/>Haskell Homepage <p>Thanks for visiting! |] addCassius [$cassius| h1 color: green ul > li:first-child border-left: 5px solid orange |] addJulius
- - START
[$julius| $(function(){ $("h1").after("<p><a href='#' id='mylink'>Never click me</a></p>"); $("#mylink").click(function(){ alert("You clicked me!!! How dare you!"); return false; }); }); |]
- - STOP
14 | OpenTopic | Basics main = warpDebug 3000 HelloWorld
Tags
Besides the whitespace rules, Hamlet also provides a few more features to make your life a little bit easier. {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout $ do
- - START
[$hamlet| <p #my-id .my-class This paragraph has an ID and a class. It also has # <b>bold \ and # <i>italic , and shows you how to control whitespace. |]
- - STOP
main = warpDebug 3000 HelloWorld Let's start on line 2. In Hamlet, instead of writing id="my-id", you can use the css-selector: #my-id. The same applies to classes: .my-class. Next, compare line 2 with lines 4 and 6: line 2 does not complete the tag, it just leaves it open (no greater than sign), while lines 4 and 6 do complete it. In Hamlet, the greater than sign is optional. However, it is required if you want to put content on the same line as the tag. The last thing to note is how we handle whitespace. On line 3, we want to force an extra space at the end of the line, so that there will be a space between the word has and the <b> tag. When we place the hash at the end of the line by itself, Hamlet ignores it entirely, and therefore only the space is kept. (If you really want to output a hash at the end of a line, simply put two, and Hamlet will ignore the second one.) advanced: Technically speaking, the trailing hash isn't necessary: if you leave a space at the end of a line, Hamlet will notice it and use it. However, some text editors will silently trim trailing whitespace, and it can be confusing to readers of your code. Therefore, using the trailing hash is recommended. The other trick is whitespace at the beginning of a line. Since Hamlet is nested using whitespace, adding extra spaces will only increase the nesting level. To fix this, on line 5, we start the line with a backslash. Once we have the backslash, the nesting level is determined, and any whitespace we see is interpreted literally. (Like the trailing hash, if you need to start a line with a backslash, just use two of them.) advanced: When it comes to tag attributes, you can write them either with or without quotes. In other words, the following lines are equivalent: <a target="_blank" href="@{MyDest}" <a target=_blank href=@{MyDest} Either way, Hamlet will generate HTML which quotes the attribute values. The only time you must use the quotes in Hamlet is when your attribute values contain whitespace.
OpenTopic | Basics | 15
Variables
There's nothing really special about the above examples: they just show some alternate syntax for HTML and CSS. The nice thing is to see how the templates are able to interact with a surrounding program. (For the moment, accept the addHamlet, addCassius and addJulius functions as magic; we will cover them when we discuss widgets.) {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout $ do
- - START
let name = "Michael" :: String let nameId = "name" :: String addHamlet [$hamlet| <h1>Hello World! <p> Welcome to my system. Your name is # <span ##{nameId}>#{name} . Enjoy your stay! <p <a href=@{HomeR}>Return Home |] addCassius [$cassius| ##{nameId} color: green |] addJulius [$julius| alert("Welcome #{name}"); |]
- - STOP
main = warpDebug 3000 HelloWorld We see two variable interpolations on line 8. The first one looks a little bit funny, but the first hash is indicating that this is an ID. In other words, it is equivalent to <span id=#{nameId}>. The second interpolation simply outputs the name into the content of the page. On line 11, we use a type-safe URL: Yesod will automatically render HomeR into a proper URL to that resource. On line 15, we reference the same nameId variable in our Cassius as we did earlier in Hamlet. This exposes a nice trick: instead of typing in identifiers in HTML and CSS directly, we can use Haskell variables. This gives two benefits:
- If you make a typo, the compiler will catch it.
- Yesod can automatically generate unique identifiers (using the newIdent function) for you, and you can then
use those to synchronize your Hamlet, Cassius and Julius code. This produces much more composable code, since you are guaranteed that names will not clash. Finally, line 20 simply let's you know that Julius can do this too.
Function Application
You can do much more than just reference variables with interpolation: you can do complete function applications. Some examples are in order: {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-}
- - START
import Yesod
16 | OpenTopic | Basics data HelloWorld = HelloWorld type Author = String type Title = String mkYesod "HelloWorld" [$parseRoutes| / HomeR GET /blog/#Author/#Title BlogR GET |] instance Yesod HelloWorld where approot _ = "" getHomeR = defaultLayout $ do let myPhrase = "ring ring ring ring ring ring ring, BANANA PHONE" :: String let myNumber = 12345 addHamlet [$hamlet| <h1>Welcome to the blog homepage. <p We highly recommend that you read a blog post on # <a href=@{BlogR "einstein" "relativity"}>general relativity . <p Also, the last 8 letters of the phrase # <i>#{myPhrase} \ are #{reverse $ take 8 $ reverse myPhrase}. <p And the first two digits of # <i>#{show myNumber} \ are #{take 2 (show myNumber)}. |]
- - STOP
getBlogR _ _ = return () main = warpDebug 3000 HelloWorld The key lines are 22, 27 and 31. You can see that:
- You can include string and numeric literals in interpolations.
- You can apply functions and constructors to values.
- Binding order works the same as in Haskell.
- You can use the dollar sign ($) operator like you can in Haskell to control binding order.
- You can also use parentheses.
The syntax for the contents of an interpolation are identical amongst Hamlet, Cassius and Julius, and do not depend
- n the type of interpolation (variable, URL or embedding).
Hamlet Control Structures
In addition to simple interpolations, Hamlet (though not Cassius or Julius) provides a few control structures, namely:
- if/elseif/else
- forall
- maybe/nothing
For all control structures, the line must begin with a dollar sign and be followed immediately by the name of the control structure. Let's see a concrete example: {-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes|/ HomeR GET|] instance Yesod HelloWorld where approot _ = ""
OpenTopic | Basics | 17 getHomeR = defaultLayout $ do
- - START
let people = ["Michael", "Miriam", "Eliezer", "Gavriella"] :: [String] let isFather x = x == "Michael" isMother x = x == "Miriam" let getAge "Michael" = Just 26 getAge "Eliezer" = Just 3 getAge _ = Nothing [$hamlet| <h1>People <ul $forall person <- people <li> <b>#{person} \ # $if isFather person This is the father of the family. $elseif isMother person This is the mother of the family. $else This is a child. \ # $maybe age <- getAge person This person is #{show age} years old. $nothing I do not know how old this person is. |]
- - STOP
main = warpDebug 3000 HelloWorld Line 10 shows the syntax of a forall. In particular, we mimick the variable binding syntax of do-notation to grab each variable from a list. Also, the pattern of li inside of $forall inside of ul is a very common one in Hamlet. Lines 15 through 20 show usage of if/elseif/else. There is no variable binding here, and usage is fairly straight-
- forward. Notice that as long as we are inside the $forall block, we can refer to the person variable.
Finally, 24 and 26 show us maybe and nothing. Like forall, maybe uses do-notation variable binding for its value. On the other hand, nothing does not have any values associated, and therefore no binding is performed.
Comments
FIXME
Templates in External Files
Quasi-quoting your templates can be convenient, as no extra files are needed, your template is close to your code, and recompilation happens automatically whenever your template changes. On the other hand, this also clutters your Haskell code with templates and requires a recompile for any change in the template. Hamlet provides two sets of functions for including an external template:
- hamletFile/cassiusFile/juliusFile
- hamletFileDebug/cassiusFileDebug/juliusFileDebug
What's very nice about these is that they have the exact same type signature, so they can be exchanged without changing your code otherwise. These functions are not exported by the Yesod module and must be imported directly from their respective modules (Text.Hamlet, Text.Cassius, Text.Julius). You'll see in a second why that is. Usage is very straight-forward. Assuming there is a Hamlet template stored in "my-template.hamlet", you could write: defaultLayout $(hamletFile "my-template.hamlet")
18 | OpenTopic | Basics For those not familiar, the dollar sign and parantheses indicate a Template Haskell interpolation. Using the second set
- f functions, the above would become:
defaultLayout $(hamletFileDebug "my-template.hamlet") Note that in order to use this, you need to enable the TemplateHaskell language extension. You can do so by adding the following line to the top of your source file: {-# LANGUAGE TemplateHaskell #-} So why do we have two sets of functions? The first fully embeds the contents of the template in the code at compile time and never looks at the template again until a recompile. This is ideal for a production environment: compile your code and you have no runtime dependency on any template files. It also avoids a runtime penalty of needing to read a file. The debug set of functions is intended for development. These functions work a little bit of magic: at compile time, they inspect your template, determine which variables they reference, and generate some Haskell code to load up those variables. At run time, they read in the template again and feed in those variables. This has a number of implications:
- Changes to your template become immediately visible upon saving the file, no recompile required.
- If you introduce new variables to the template that were not there before, you'll need to recompile. This might
require you manually nudging GHC to recompile the Haskell file, since it won't think anything has changed.
- Due to some of the tricks needed to pull this off, some of the more corner cases of templates are not supported.
For example, using a forall to bind a function to a variable. This is an obscure enough case that it shouldn't be an issue. This is also the reason why Yesod does not export these functions by default. The Yesod scaffolding tool creates a Settings.hs file which exports these functions, in a slightly modified form, and chooses whether to use the debug or regular version based upon build flags. Long story short: it automatically uses the debug version during development and non-debug version during production. Excepting very short templates, this is probably how you'll write most of your templates in Yesod. The typical file structure is to create hamlet, cassius and julius folders and place the respective templates in each. Each template has a filename extension matching the template language. In other words, you'd typically have: # hamlet/homepage.hamlet <h1>Hello World! # cassius/homepage.cassius h1 color: green # julius/homepage.julius alert("Don't you hate it when you get an alert when you open a page?"); # Settings.hs, paraphrasing import qualified Text.Hamlet import qualified Text.Cassius import qualified Text.Julius hamletFile x = Text.Hamlet.hamletFileDebug $ "hamlet/" ++ x ++ ".hamlet"
- - same for cassius and julius
- - when moving to production, you would just remove Debug
# And finally your handler code import Settings getHomeR = defaultLayout $ do setTitle "Homepage" addHamlet $(hamletFile "homepage") addCassius $(cassiusFile "homepage") addJulius $(juliusFile "homepage") For simplicity, most of the examples in this book will use quasi-quoted syntax. Just remember that you can always swap this out for external files.
OpenTopic | Basics | 19
Types
I have purposely skirted the issue of what the value of these templates is. Let's start off with Cassius: Text.Cassius defines a datatype called Css. Then the value of a cassius template is: type Cassius url = (url -> [(String, String)] -> String) -> Css That's a little bit intimidating, so let's break it down. Cassius takes a type parameter, url, which is the datatype of our type-safe URL. A Cassius value itself is a function: the argument to the function is a URL rendering function, which given a type-safe URL value and a list of query string parameters, produces a URL. Using that, a Cassius value can produce a Css value. Yesod itself knows how to apply the URL rendering function and unwrap the Css value, so unless you want to dig under the surface, you won't need to get your hands dirty. Julius is almost identical. Instead of Css, it defines a type Javascript, and then has a datatype: type Julius url = (url -> [(String, String)] -> String) -> Javascript Now, you're probably expecting me to say that the same holds true for Hamlet. Well... it sort of does. Instead of defining its own Html datatype, Hamlet borrows the datatype from blaze-html. But then it does define a Hamlet type synonym: type Hamlet url = (url -> [(String, String)] -> String) -> Html However, Hamlet has an extra feature that Cassius and Julius don't have: polymorphism. This means that a Hamlet template can take on various values, including Html and Hamlet. Yesod defines an instance for Widget, meaning that a Hamlet template can be used directly as a widget. In fact, we have been abusing that fact every time we have written defaultLayout [$hamlet|...|] without using addHamlet. When we get to widgets, we will explore why this polymorphism is so useful. As a heads up: it really saves the day when dealing with forms.
Lucius
Cassius is a great pair to Hamlet: both indicate nesting via indentation and do away with various "line noise"
- characters. However, there are a few practical issues with Cassius:
- You cannot copy-paste a CSS file into a Cassius template.
- It's difficult to add extra features like block nesting (to be described below).
Whether or not this is a serious problem is a subjective matter. To provide Yesod users with the greatest flexibility, there is now a second CSS language: Lucius. Cassius and Lucius share a number of things: variable/URL interpolation syntax is identical, and they end up producing the same datatypes. They are therefore completely interchangeable, and one can even use them simultaneously in a single project. The difference is that Lucius is designed as a superset of CSS. It should be possible to take any valid CSS file, copy it into a Lucius file, and get the same results (albeit a bit minified). advanced: Those familiar with it might note a corrolary to the Sass/ Scss divide in the Ruby world. Besides being a CSS pass-through with variable interpolation, Lucius also intends to add convenience features. For now, this is limited to block nesting. This means that Lucius will convert: foo, bar { baz, bin { color: red; } } into foo baz, foo bin, bar baz, bar bin { color: red; }
20 | OpenTopic | Basics
Hamlet Syntax
The examples above have not covered every aspect of Hamlet. This section gives a complete overview of the Hamlet syntax for learning and reference purposes. Interpolation Hamlet supports four forms of interpolation: #{var} interpolates a normal variable, which must be an instance of ToHtml. @{url} interpolates a type-safe URL. @?{urlParams} interpolates a URL/get parameters pair. ^{template} interpolates another template. (Note: in Hamlet 0.9, we will introduce a fifth form of interpolation to handle internationalized messages.) Tags Any line that begins with a less-than sign begins a
- tag. The name of the tag must immediately follow.
(If no name is given, div is assumed.) There are then a number of whitespace-separated attributes, either given as key=value, #id or .class. Conditional attributes Both key=value and class attributes can be prefixed with a
- condition. So for example: <input
type=checkbox :isSelected:selected=true>
- r <a :isCurrent:.current
href=@{MyRouteR}>. Sealing the tag Sealing the tag is optional. However, if you do seal it, you can put some raw content after the tag. Close tag Close tags are not used at all in Hamlet. Indentation Nesting of tags and content is implied via indentation level. For these purposes, a hard tab has the value of four spaces. Conditionals You can do conditionals via $if/$elseif/$else. Loops $forall can be applied to any instance of Foldable. with binding $with can be used to bind a new variable.
Summary
Yesod has templating languages for HTML, CSS and Javascript. All of them allow variable interpolation, safe handling of URLs and embedding sub-templates. Since the code is dealt with at compile time, you can use the compiler as your friend and get strong type safety guarantees. Oh, and XSS vulnerabilities get handled automatically. There are three ways to embed the templates: through quasi-quotation, regular external and debug external. Quasi- quotation is great for small, simple templates that won't be changing often. Debug mode is great for development, and since it has the same type signature as the regular external functions, you can easily switch to using them for your production code.
Widgets
One of the challenges in web development is that we have to coordinate three different client-side technologies: HTML, CSS and Javascript. Worse still, we have to place these components in different locations on the page: CSS
OpenTopic | Basics | 21 in a style tag in the head, Javascript in a script tag in the head, and HTML in the body. And never mind if you want to put your CSS and Javascript in separate files! In practice, this works out fairly nicely when building a single page, because we can separate our structure (HTML), styling (CSS) and logic (Javascript). But when we want to build modular pieces of code that can be easily composed, it can be a headache to coordinate all three pieces separately. Widgets are Yesod's solution to the problem. They also help with the issue of including libraries, such as jQuery, one time only. Our three template languages- Hamlet, Cassius and Julius- provide the raw tools for constructing your output. Widgets provide the glue that allows them to work together seamlessly.
What's in a Widget?
At a very superficial level, an HTML document is just a bunch of nested tags. This is the approach most HTML generation tools take: you simply define hierarchies of tags and are done with it. But let's imagine that I want to write a component of a page for displaying the navbar. I want this to be "plug and play": I simply call the function at the right time, and the navbar is inserted at the correct point in the hierarchy. This is where our superficial HTML generation breaks down. Our navbar likely consists of some CSS and JavaScript in addition to HTML. By the time we call the navbar function, we have already rendered the <head> tag, so it is too late to add a new <style> tag for our CSS declarations. Under normal strategies, we would need to break up our navbar function into three parts: HTML, CSS and JavaScript, and make sure that we always call all three pieces. Widgets take a different approach. Instead of viewing an HTML document as a monolithic tree of tags, widgets see a number of distinct components in the page. In particular:
- The title
- External stylesheets
- External Javascript
- CSS declarations
- Javascript code
- Arbitrary <head> content
- Arbitrary <body> content
Different components have different semantics. For example, there can only be one title, but there can be multiple external scripts and stylesheets. However, those external scripts and stylesheets should only be included once. Arbitrary head and body content, on the other hand, has no limitation (someone may want to have five lorem ipsum blocks after all). The job of a widget is to hold onto these disparate components and apply proper logic for combining different widgets
- together. This consists of things like taking the first title set and ignoring others, applying nub to the list of external
scripts and stylesheets, and simply concatenating head and body content. advanced: In general, you should avoid nub since it has very bad performance. Usually when you are looking for uniqueness, you do not care about order, and therefore map head . group . sort is more efficient than a call to nub. However, in our case, order is important: we would not want to include jQuery UI before we include
- jQuery. Therefore, we are stuck with nub.
Building Widgets
In the templates chapter, we already began an initial look at how to construct widgets. Let's begin more formally here. Widgets have a monad instance, so we can use do notation for building up larger widgets from smaller parts. For each
- f the components of a widget listed above, there is a corresponding primitive.
Let's see how we can combine two simple primitives: setTitle and addHtml: {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod
22 | OpenTopic | Basics data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes| / HomeR GET |] instance Yesod HelloWorld where approot _ = ""
- - START
getHomeR = defaultLayout $ do setTitle "Hello World" addHtml [$hamlet|Hello World!|]
- - STOP
main = warpDebug 3000 HelloWorld The one thing to note is that setTitle takes a value of type Html as an argument, not a String. As usual, the recommendation is to run your code with OverloadedStrings turned on. FIXME The rest of this chapter is under construction. Polymorphic Hamlet We have already mentioned that a Hamlet template is polymorphic. This is best seen in the context of widgets. {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TemplateHaskell #-} import Yesod data HelloWorld = HelloWorld mkYesod "HelloWorld" [$parseRoutes| / HomeR GET |] instance Yesod HelloWorld where approot _ = ""
- - START
getHomeR = defaultLayout $ do setTitle "Polymorphic Hamlet" addHtml [$hamlet|<p>I was added with addHtml|] addHamlet [$hamlet|<p>I was added with addHamlet|] addWidget [$hamlet|<p>I was added with addWidget|]
- - STOP
main = warpDebug 3000 HelloWorld As you can see, all three functions addHtml, addHamlet and addWidget can work on a Hamlet template. The question arises: why do we need three? One reason is that while a Hamlet template itself is polymorphic, not all values are. For example, you may have a user-supplied Html value that you wish to add to your page: addHamlet and addWidget will not work; you will have to use addHtml. However, there is a more subtle point as well. While in theory a Hamlet template is polymorphic, this actually depends upon what values are embedded inside it (using ^{...} interpolation). For example, [$hamlet| ^{someValue}|] will have the same type as someValue; if someValue is of type Hamlet, then the template will be as well.
Yesod Typeclass
Every one of our Yesod applications requires an instance of the Yesod typeclass. So far, we've only seen the approot method, with the cryptic definition approot _ = "", and defaultLayout. In this chapter, we'll explore the meaning of the approot method, along with many other methods in the Yesod typeclass. The Yesod typeclass gives us a central place for defining settings for our application. Excluding the approot method, everything else has a default definition which is usually the right thing. But in order to build a powerful, customized application, you'll usually end up overriding at least a few of these methods.
OpenTopic | Basics | 23
Rendering and Parsing URLs
We've already mentioned how Yesod is able to automatically render type-safe URLs into a textual URL that can be inserted into an HTML page. Let's say we have a route definition that looks like: mkYesod "MyApp" [$parseRoutes| /some/path SomePathR GET ] If we place SomePathR into a hamlet template, how does Yesod render it? Yesod always tries to construct absolute
- URLs. This is especially useful once we start creating XML sitemaps and Atom feeds, or sending emails. But in order
to construct an absolute URL, we need to know the domain name of the application. You might think to just get that information from the user's request, but we still need to deal with ports. And even if we get the port number from the request, are we using HTTP or HTTPS? And even if you know that, such an approach would break one of our RESTful principles: depending on how the user submitted a request would generate different URLs. For example, we would generate different URLs depending if the user connected to "example.com"
- r "www.example.com".
And finally, Yesod doesn't make any assumption about where you host your application. For example, I may have a mostly static site (http://static.example.com/), but I'd like to stick a Yesod-powered Wiki at /wiki/. There is no reliable way for an application to determine what subpath it is being hosted from. So instead of doing all of this guesswork, Yesod needs you to tell it the application root. So using the wiki example, you would write your Yesod instance as: instance Yesod MyWiki where approot _ = "http://static.example.com/wiki" Notice that there is no trailing slash there. Next, when Yesod wants to construct a URL for SomePathR, it determines that the relative path for SomePathR is "/some/path", appends that to your approot and creates "http:// static.example.com/wiki/some/path". This also explains our cryptic approot _ = "": for our examples in the book, we're always serving from the root
- f the domain (in our case, localhost). By using an empty string, SomePathR renders to "/some/path", which works
just fine. In real life applications, however, you should use a real application root. And by the way, the site template generated by the scaffolding tool automatically uses conditional compilation to switch between development and production builds, so you can easily test on one domain- like localhost- and serve from a different domain. advanced: You might be wondering: why does approot take that first argument if it is always ignored? There are two reasons:
- It is needed by Haskell's type system to determine
which instance of Yesod to use for grabbing the typeclass.
- And actually, the first argument is not always ignored.
For example, if you want to load the application root value from a configuration file, the most logical place to store that value is in the foundation datatype, and then for the approot function to grab the value from there. joinPath In order to convert a type-safe URL into a text value, Yesod uses two helper functions. The first is the renderRoute method of the RenderRoute typeclass. Every type-safe URL is an instance of this typeclass. renderRoute simply converts a value into a list of path pieces. For example, our SomePathR from above would be converted into ["some", "path"].
24 | OpenTopic | Basics advanced: Actually, renderRoute produces both the path pieces and a list of query-string parameters. The default instances of renderRoute always provide an empty list of query string
- parameters. However, it is possible to override this. One
notable case is the static subsite, which puts a hash of the file contents in the query string for caching purposes. The other function is the joinPath method of the Yesod typeclass. This function takes the four arguments: the foundation value, the application root, a list of path segments and a list of query string parameters, and returns a textual URL. The default implementation does the "right thing": it separates the path pieces by forward slashes, prepends the application root and appends the query string. If you are happy with default URL rendering, you should not need to modify it. However, if you want to modify URL rendering to do things like append a trailing slash, this would be the place to do it. cleanPath The flip side to joinPath is cleanPath. Let's look at how it gets used in the dispatch process:
- 1. The path info requested by the user is split into a series of path pieces.
- 2. If any prefix of the path pieces matches a subsite, then dispatching is passed off to the subsite.
- 3. Otherwise, we pass the path pieces to the cleanPath function.
- 4. If cleanPath indicates a redirect (a Left response), then a 301 response is sent to the client. This is used to force
canonical URLs (eg, remove extra slashes).
- 5. Otherwise, we try to dispatch to our non-subsite routes using the response from cleanPath (a Right). If this works,
we return a response. Otherwise, we return a 404. This combination allows subsites to retain full control of how their URLs appear, yet allows master sites to have modified URLs. As a simple example, let's see how we could modify Yesod to always produce trailing slashes on URLs:
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-} import Yesod import Web.Routes (encodePathInfo) import Blaze.ByteString.Builder.Char.Utf8 (fromText) import qualified Data.Text as T import Control.Arrow ((***)) data Slash = Slash mkYesod "Slash" [$parseRoutes| / RootR GET /foo FooR GET |] instance Yesod Slash where approot _ = "" joinPath _ ar pieces' qs' = fromText $ ar `T.append` T.pack ('/' : encodePathInfo pieces qs) where qs = map (T.unpack *** T.unpack) qs' pieces = map T.unpack $ if null pieces' then [] else pieces' ++ [""]
- - We want to keep canonical URLs. Therefore, if the URL is missing a
- - trailing slash, redirect. But the empty set of pieces always stays the
- - same.
cleanPath _ [] = Right [] cleanPath _ s
OpenTopic | Basics | 25 | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one Right $ init s
- - Since joinPath will append the missing trailing slash, we simply
- - remove empty pieces.
| otherwise = Left $ filter (not . T.null) s getRootR = defaultLayout [$hamlet| <p <a href=@{RootR}>RootR <p <a href=@{FooR}>FooR |] getFooR = getRootR main = warpDebug 3000 Slash First, let's look at our joinPath implementation. This is copied almost verbatim from the default Yesod implementation, with one difference: when the initial pieces is not empty, we append an extra empty string to the
- end. When dealing with path pieces, an empty string will simply append another slash. So adding an extra empty
string will force a trailing slash. The only time to not want this trailing slash is when we are looking at the root of the application, because adding an extra slash will result in two slashes in a row. cleanPath is a little bit trickier. First, we check for the empty path like before, and if so pass it through as-is. We use Right to indicate that a redirect is not necessary. The next clause is actually checking for two different possible URL issues:
- There is a double slash, which would show up as an empty string in the middle of our paths.
- There is a missing trailing slash, which would show up as the last piece not being an empty string.
Assuming neither of those conditions hold, then only the last piece is empty, and we should dispatch based on all but the last piece. However, if this is not the case, we want to redirect to a canonical URL. In this case, we strip out all empty pieces and do not bother appending a trailing slash, since joinPath will do that for us.
defaultLayout
Most websites like to apply some general template to all of their pages. defaultLayout is the recommended approach for this. While you could just as easily define your own function and call that instead, when you override defaultLayout all of the Yesod-generated pages (error pages, authentication pages) automatically get this style. Overriding is very straight-forward: we use widgetToPageContent to convert a Widget to a title, head tags and body tags, and then use hamletToRepHtml to convert a Hamlet template into a RepHtml. We can even use widget functions like addCassius from within defaultLayout. A simple example should make this all clear: {-# LANGUAGE TypeFamilies, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod data Layout = Layout mkYesod "Layout" [$parseRoutes|/ RootR GET|] instance Yesod Layout where approot _ = ""
- - START
defaultLayout contents = do PageContent title headTags bodyTags <- widgetToPageContent $ do addCassius [$cassius| #body font-family: sans-serif #wrapper width: 760px margin: 0 auto |] addWidget contents hamletToRepHtml [$hamlet|
26 | OpenTopic | Basics !!! <html> <head> <title>#{title} ^{headTags} <body> <div id="wrapper"> ^{bodyTags} |]
- - STOP
getRootR = defaultLayout $ do setTitle $ string "Root test" addCassius [$cassius|body color: red |] addHamlet [$hamlet|<h1>Hello |] main = warpDebug 4000 Layout advanced: The three exclamation points (!!!) on a line by themselves is used to insert a doctype statement in Hamlet. By default, this will be the HTML 5 doctype line, ie <!DOCTYPE html>. You should make certain to only insert this once per output. In most use cases, the only place you will need it is in your defaultLayout function. getMessage Even though we haven't covered sessions yet, I'd like to mention getMessage here. A common pattern in web development is needed to set a message in one handler and display it in another. For example, if a user POSTs a form, you may want to redirect him/her to another page along with a "Form submission complete" message. To facilitate this, Yesod comes built in with a pair of functions: setMessage sets a message in the user session, and getMessage retrieves the message (and clears it, so it doesn't appear a second time). It's recommended that you put the result of getMessage into your defaultLayout. For example: {-# LANGUAGE TypeFamilies, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod data Layout = Layout mkYesod "Layout" [$parseRoutes| / RootR GET /msg MsgR GET |] instance Yesod Layout where approot _ = ""
- - START
defaultLayout contents = do PageContent title headTags bodyTags <- widgetToPageContent contents mmsg <- getMessage hamletToRepHtml [$hamlet| !!! <html> <head> <title>#{title} ^{headTags} <body> $maybe msg <- mmsg <div #message>#{msg}
OpenTopic | Basics | 27 ^{bodyTags} |]
- - STOP
getRootR = defaultLayout [$hamlet|<a href="@{MsgR}">message|] getMsgR = setMessage (string "foo") >> redirect RedirectTemporary RootR >> return () main = warpDebug 4000 Layout We'll cover getMessage/setMessage in more detail when we discuss sessions.
Custom error pages
One of the marks of a professional web site is a properly designed error page. Yesod gets you a long way there by automatically using your defaultLayout for displaying error pages. But sometimes, you'll want to go even further. For this, you'll want to override the errorHandler method: {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod data Layout = Layout mkYesod "Layout" [$parseRoutes|/ RootR GET|] instance Yesod Layout where approot _ = ""
- - START
errorHandler NotFound = fmap chooseRep $ defaultLayout $ do setTitle "Request page not located" addWidget [$hamlet| <h1>Not Found <p>We appologize for the inconvenience, but the requested page could not be located. |] errorHandler other = defaultErrorHandler other
- - STOP
getRootR = defaultLayout [$hamlet|\Hello World |] main = warpDebug 4000 Layout Here we specify a custom 404 error page. We can also use the defaultErrorHandler when we don't want to write a custom handler for each error type. Due to type constraints, we need to start off our methods with "fmap chooseRep", but otherwise you can write a typical handler function. In fact, you could even use special responses like redirects: {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod data Layout = Layout mkYesod "Layout" [$parseRoutes|/ RootR GET|] instance Yesod Layout where approot _ = ""
- - START
errorHandler NotFound = redirect RedirectTemporary RootR errorHandler other = defaultErrorHandler other
- - STOP
getRootR = defaultLayout [$hamlet|\Hello World |] main = warpDebug 4000 Layout Note: Even though you can do this, I don't actually recommend such practices. A 404 should be a 404.
28 | OpenTopic | Basics
Summary
The Yesod typeclass has a number of overrideable methods that allow you to configure your application. Besides approot, they are all optional. By using built-in Yesod constructs like defaultLayout and getMessage, you'll get a consistent look-and-feel throughout your site, including pages automatically generated by Yesod such as error pages and authentication. We haven't covered all the methods in the Yesod typeclass in this chapter. Some of them relate to topics we have not yet covered (such as authentication), and will be discussed in those chapters. For a full listing of methods available, you should always consult the Haddock documentation.
Routing and Handlers
If we look at Yesod as a Model-View-Controller framework, routing and handlers make up the controller. For contrast, let's describe two other routing approaches used in other web development environments:
- Dispatch based on file name. This is how PHP and ASP work, for example.
- Have a centralized routing function that parses routes based on regular expressions. Django and Rails follow this
approach. Yesod is closer in principle to the latter technique. Even so, there are significant differences. Instead of using regular expressions, Yesod matches on pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod has an intermediate data type (called the route datatype, or a type-safe URL) and creates two-way conversion functions. Coding this more advanced system manually is tedious and error prone. Therefore, Yesod relies heavily on Template Haskell and Quasi-Quotation to automatically generate this code for you. This chapter will explain the syntax of the routing declarations, give you a glimpse of what code is generated for you, and explain the interaction between routing and handler functions.
Route Syntax
Pieces The first thing Yesod does when it gets a request (well, maybe not the first) is split up the requested path into pieces. The pieces are simply tokenized at all forward slashes. So: toPieces "/" = [] toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""] You may notice that there are some funny things going on with trailing slashes, or double slashes ("/foo//bar//"), or a few other things. Yesod believes in having canonical URLs; if someone requests a URL with a trailing slash, or with a double slash, they automatically get a redirect to the canonical version. This follows the RESTful principle of one URL for one resource, and can help with your search rankings. What this means for you is that you needn't concern yourself with the exact structure of your URLs: you can safely think about pieces of a path, and Yesod automatically handles intercalating the slashes and escaping problematic characters. If, by the way, you want more fine-tuned control of how paths are split into pieces and joined together again, you'll want to look at the cleanPath and joinPath methods in the Yesod typeclass chapter. Types of Pieces When you are declaring your routes, you have three types of pieces at your disposal: Static This is a plain string that must be matched against precisely in the URL. Dynamic single This is a single piece (ie, between two forward slashes), but can be a user-submitted value. This is the primary method of receiving extra user input
OpenTopic | Basics | 29
- n a page request. These pieces begin with a hash
(#) and are followed by a data type. The datatype must be an instance of SinglePiece. Dynamic multi The same as before, but can receive multiple pieces of the URL. This must always be the last piece in a resource pattern. It is specified by an asterisk (*) followed by a datatype, which must be an instance of MultiPiece. Multi pieces are not as common as the other two, though they are very important for implementing features like static trees representing file structure or wikis with arbitrary hierarchies. Let us take a look at some standard kinds of resource patterns you may want to write. Starting simply, the root of an application will just be /. Similarly, you may want to place your FAQ at /page/faq. Now let's say you are going to write a Fibonacci website. You may construct your URLs like /fib/#Int. But there's a slight problem with this: we do not want to allow negative numbers or zero to be passed into our application. Fortunately, the type system can protect us: {-# LANGUAGE TypeFamilies, QuasiQuotes, GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod import qualified Data.Text as T import Web.Routes.Quasi data Fibs = Fibs
- - START
newtype Natural = Natural Int -- we might even like to go with Word here
- - STOP
deriving (Show, Read, Eq, Num, Ord)
- - START
instance SinglePiece Natural where toSinglePiece (Natural i) = T.pack $ show i fromSinglePiece s = case reads $ T.unpack s of (i, _):_ | i < 1 -> Nothing | otherwise -> Just $ Natural i [] -> Nothing
- - STOP
mkYesod "Fibs" [$parseRoutes| /fibs/#Natural FibsR GET |] instance Yesod Fibs where approot _ = "" fibs = 1 : 1 : zipWith (+) fibs (tail fibs) getFibsR :: Natural -> GHandler Fibs Fibs RepPlain getFibsR (Natural i) = return $ RepPlain $ toContent $ show $ fibs !! (i - 1) main = warpDebug 3000 Fibs On line 1 we define a simple newtype wrapper around Int to protect ourselves from invalid input. We can see that SinglePiece is a typeclass with two methods. toSinglePiece does nothing more than convert to a Text. fromSinglePiece attempts to convert a Text to our datatype, returning Nothing when this conversion is
- impossible. By using this datatype, we can ensure that our handler function is only ever given natural numbers,
allowing us to once again use the type system to battle the boundary issue. Defining a MultiPiece is just as simple. Let's say we want to have a Wiki with at least two levels of hierarchy; we might define a datatype such as: {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod
30 | OpenTopic | Basics import Data.Text (Text) import Web.Routes.Quasi data Fibs = Fibs
- - START
data Page = Page Text Text [Text] -- 2 or more instance MultiPiece Page where toMultiPiece (Page x y z) = x : y : z fromMultiPiece (x:y:z) = Just $ Page x y z fromMultiPiece _ = Nothing
- - STOP
main = return () Resource name Each resource pattern also has a name associated with it. That name will become the constructor for the type safe URL datatype associated with your application. Therefore, it has to start with a capital letter. By convention, these resource names all end with a capital R. There is nothing forcing you to do this, it is just common practice. The exact definition of our constructor depends upon the resource pattern it is attached to. Whatever datatypes are included in single and multi pieces of the pattern become arguments to the datatype. This gives us a 1-to-1 correspondence between our type safe URL values and valid URLs in our application. advanced: This doesn't necessarily mean that every value is a working page, just that it is is a potentially valid URL. As an example, that value PersonR "Michael" may not resolve to a valid page if there is no Michael in the database. Let's get some real examples going here. If you had the resource patterns /person/#String named PersonR, /year/#Int named YearR and /page/faq named FaqR, you would end up with a route data type roughly looking like: data MyRoute = PersonR String | YearR Int | FaqR If a user requests the relative URL of /year/2009, Yesod will convert it into the value YearR 2009. /person/ Michael becomes PersonR "Michael" and /page/faq becomes FaqR. On the other hand, /year/two- thousand-nine, /person/michael/snoyman and /page/FAQ would all result in 404 errors without ever seeing your code. advanced: Throughout the above discussion, I used the terms type- safe URLs and routes datatype interchangeably. Don't be confused, they are the exact same thing. They just sound better in different contexts. Handler specification The last piece of the puzzle when declaring your resources is how they will be handled. There are three options in Yesod:
- You have a single handler function which should be used for all request methods.
- You want to write a separate handler function for each request method you will support. All other request method
will generate a 405 Bad Method response.
- You want to pass off to a subsite.
The first two are very easily specified. A single handler function will be a line with just a resource pattern and the resource name, such as /page/faq FaqR. In this case, the handler function must be named handleFaqR.
OpenTopic | Basics | 31 A separate handler for each request method will be the same, plus a list of request methods. The request methods must be ALL CAPITAL LETTERS. For example, /person/#String PersonR GET POST DELETE. In this case, you would need to define the three handler functions getPersonR, postPersonR and deletePersonR. Subsites are a very useful— but complicated— topic in Yesod. We will cover writing subsites later, but using them is not too difficult. The most commonly used subsite is the static subsite, which serves static files for your application. In order to serve static files from /static, you would need a resource line like: /static StaticR Static getStatic In this line, /static just says where in your URL structure to serve the static files from. There is nothing magical about the word static, you could easily replace it with /my/non-dynamic/files. The next word, StaticR, gives the resource name. The next two words are what specify that we are using a subsite. Static is the name of the subsite foundation datatype, and getStatic is a function that gets a Static value from a value of your main application's foundation datatype. Let's not get too caught up in the details of subsites now. We will look more closely at the static subsite in the scaffolded site chapter.
Dispatch
Once you have specified your routes, Yesod will take care of all the pesky details of dispatch for you. You just need to make sure to provide the appropriate handler functions. For subsite routes, you do not need to write any handler functions, but you do for the other two. We mentioned the naming rules above (MyHandlerR GET -> getMyHandlerR, MyOtherHandlerR -> handleMyOtherHandlerR). Now we need the type signature. Return Type Let's look at a simple handler function: {-# LANGUAGE TypeFamilies, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod data Simple = Simple
- - START
mkYesod "Simple" [$parseRoutes| / HomeR GET |] getHomeR :: GHandler subsite Simple RepHtml getHomeR = defaultLayout [$hamlet|<h1>This is simple |]
- - STOP
instance Yesod Simple where approot _ = "" main = warpDebug 3000 Simple Look at the type signature of getHomeR. Unfortunately, there is a lot of complexity here to dig through. We will cover the GHandler monad in more detail later. Next come subsite and Simple. This is where we need to start heading down the rabbit hole... The same way your application has a foundation datatype and associated type-safe URL datatype, so does every
- subsite. As an example, the Static subsite's foundation datatype has information on how to look up a requested file.
The Authentication subsite has a URL datatype that provides login, logout and various other actions. The GHandler monad needs information on both the current subsite and current master site. Most of the time, these are the same! When you are writing your typical handler functions, you only have a single foundation going on. So in the code snippet above, the type signature for getHomeR could also have been GHandler Simple Simple RepHtml without any problems. For now, just accept it as a strange quirk that you need to deal with this extra type parameter. In fact, it is recommended to use a type synonym like type Handler = GHandler MyApp MyApp at the beginning of
32 | OpenTopic | Basics your code (the scaffolded site does this for you). And when we get to the subsite chapter we will explore why this awkwardness is a necessity, and the huge benefits we reap as a result. ChooseRep That just leaves us one thing: RepHtml. When we discuss representations we will explore the why of things more; for now, we are just interested in the how. As you might guess, RepHtml is a datatype for HTML responses. But as you also may guess, web sites need to return responses besides HTML. CSS, Javascript, images, XML are all necessities of a website. Therefore, the return value
- f a handler function can be any instance of HasReps.
HasReps is a powerful concept that allows Yesod to automatically choose the correct representation of your data based on the client request. For now, we will focus just on simple instances such as RepHtml, which only provide one representation. Arguments But not every route is as simple as the HomeR we just defined. Take for instance our PersonR route from earlier. The name of the person needs to be passed to the handler function. This translation is very straight-forward, and hopefully
- intuitive. For example:
{-# LANGUAGE TypeFamilies, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} import Yesod import Data.Text (Text) import qualified Data.Text as T data Args = Args
- - START
type Handler = GHandler Args Args type Texts = [Text] mkYesod "Args" [$parseRoutes| /person/#Text PersonR GET /year/#Integer/month/#Text/day/#Int DateR /wiki/*Texts WikiR GET |] getPersonR :: Text -> Handler RepHtml getPersonR name = defaultLayout [$hamlet|<h1>Hello #{name}!|] handleDateR :: Integer -> Text -> Int -> Handler RepPlain -- text/plain handleDateR year month day = return $ RepPlain $ toContent $ T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year] getWikiR :: [Text] -> Handler RepPlain getWikiR = return . RepPlain . toContent . T.unwords
- - STOP
instance Yesod Args where approot _ = "" main = warpDebug 3000 Args The arguments have the types of the dynamic pieces for each route, in the order specified. Also, notice how we are able to use both RepHtml and RepPlain.
The GHandler Monad
The vast majority of code you write in Yesod sits in the GHandler monad. If you are approaching this from an MVC (MVC) background, your GHandler code is the Controller. Some important points to know about GHandler:
- It is an instance of MonadIO, so you can run any IO action in your handlers with liftIO. By the way, liftIO
is exported by the Yesod module for your convenience.
- GHandler is really a monad transformer stack providing a number of different components.
OpenTopic | Basics | 33
- A Reader component provides access to immutable information about a request and the environment: the
foundation value, request headers and much more.
- A Writer component allows you to add extra response headers.
- A State component deals with session variables, allowing them to be both read, written and deleted. Sessions are
discussed in their own chapter.
- An Error component deals with short-circuiting. Despite the name, these are not necessarily errors: you can use
this for sending static files and redirecting in addition to sending error responses. advanced: And on top of all this, there is in fact a larger generalization from GHandler: a GGHandler. While a GHandler wraps around an Iteratee, which allows it to read the request body, a GGHandler can wrap around any monad, including directly around IO. This generalization is necessary for dealing with catching exceptions. We will discuss this in more depth later. The remainder of this chapter will give a brief introduction to some of the most common functions living in the GHandler monad. I am specifically not covering any of the session functions; that will be addressed in the sessions chapter. Application Information There are a number of functions that return information about your application as a whole, and give no information about individual requests. Some of these are: getYesod Returns your applicaton foundation value. If you store configuration values in your foundation, you will probably end up using this function a lot. getYesodSub Get the subsite foundation value. Unless you are working in a subsite, this will return the same value as getYesod. getUrlRender Returns the URL rendering function, which converts a type-safe URL into a String. Most
- f the time- like with Hamlet- Yesod calls this
function for you, but you may occassionally need to call it directly. getUrlRenderParams A variant of getUrlRender that converts both a type-safe URL and a list of query-string
- parameters. This function handles all percent-
encoding necessary. Request Information The most common information you will want to get about the current request is the requested path, the query string parameters and POSTed form data. The first of those is dealt with in the routing, as described above. The other two are best dealt with using the forms module. That said, you will sometimes need to get the data in a more raw format. For this purpose, Yesod exposes the Request datatype along with the getRequest function to retrieve it. This gives you access to the full list of GET parameters, cookies, and preferred languages. There are some convenient functions to make these lookups easier, such as lookupGetParam, lookupCookie and languages. For raw access to the POST parameters, you should use runRequest. If you need even more raw data, like request headers, you can use waiRequest to access the WAI request value. Short Circuiting The following functions immediately end execution of a handler function and return a result to the user.
34 | OpenTopic | Basics redirect Sends a redirect response to the user. You can specify whether you want a 301, 302 or 303 status
- code. This function takes a type-safe URL as a
- destination. There are also redirectString
and redirectParams variants. notFound Return a 404 response. This can be useful if a user requests a database value that doesn't exist. permissionDenied Return a 403 response with a specific error message. invalidArgs A 400 response with a list of invalid arguments. sendFile Sends a file from the filesystem with a specified content type. This is the preferred way to send static files, since the underlying WAI handler may be able to optimize this to a sendfile system
- call. Using readFile for sending static files
should not be necessary. sendResponse Send a normal HasReps response with a 200 status
- code. This is really just a convenience for when
you need to break out of some deeply nested code with an immediate response. Response Headers setCookie Set a cookie on the client. Instead of taking an expiration date, this function takes a cookie duration in minutes. Remember, you won't see this cookie using lookupCookie until the following request. deleteCookie Tells the client to remove a cookie. Once again, lookupCookie will not reflect this change until the next request. setHeader Set an arbitrary response header. setLanguage Set the preferred user language, which will show up in the result of the languages function. cacheSeconds Set a Cache-Control header to indicate how many seconds this response can be cached. This can be particularly useful if you are using varnish on your server. neverExpires Set the Expires header to the year 2037. You can use this with content which should never expire, such as when the request path has a hash value associated with it. alreadyExpired Sets the Expires header to the past. expiresAt Sets the Expires header to the specified date/time.
Summary
Routing and dispatch is arguably the core of Yesod: it is from here that our type-safe URLs are defined, and the majority of our code is written within the GHandler monad. This chapter covered some of the most important and central concepts of Yesod, so it is important that you properly digest it.
OpenTopic | Basics | 35 This chapter also hinted at a number of more complex Yesod topics that we will be covering later. But you should be able to write some very sophisticated web applications with just the knowledge you have learned up until here.
Basic Forms
I've mentioned the boundary issue already: whenever data enters or leaves an application, we need to validate our
- data. Probably the most difficult place this occurs is forms. Coding forms is complex; in an ideal world, we'd like a
solution that addresses the following problems:
- Ensure data is valid.
- Marshal string data in the form submission to Haskell datatypes.
- Generate HTML code for displaying the form.
- Generate Javascript to do clientside validation and provide more user-friendly widgets, such as date pickers.
- Build up more complex forms by combining together simpler forms.
The form system used in Yesod is built around many of the concepts in formlets; if you are familiar with that work, it will give you a head start here. In this chapter, we'll start with some real-life examples of using forms, and then get into the low-level details of how they work so you can abuse their full power. advanced: The examples below all assume that you have the OverloadedStrings extension enabled. To do so, simply put the following line at the beginning of your code: {-# LANGUAGE OverloadedStrings #-}
Random bananas
Let's start off with a silly example: we want to create a website that generates a random number of some object. It needs to know the minimum number of objects, the maximum number of objects, the name of a single object and the name of a plural object (eg, book and books, mouse and mice).
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod import System.Random import Control.Applicative import Data.Text (Text) import qualified Data.Text as T data Rand = Rand type Handler = GHandler Rand Rand mkYesod "Rand" [$parseRoutes| / RootR GET |] instance Yesod Rand where approot _ = "" data Params = Params { minNumber :: Int , maxNumber :: Int , singleWord :: Text , pluralWord :: Text } paramsFormlet :: Maybe Params -> Form s m Params
- - Same as: paramsFormlet :: Formlet s m Params
paramsFormlet mparams = fieldsToTable $ Params <$> intField "Minimum number" (fmap minNumber mparams)
36 | OpenTopic | Basics <*> intField "Maximum number" (fmap maxNumber mparams) <*> stringField "Single word" (fmap singleWord mparams) <*> stringField "Plural word" (fmap pluralWord mparams) getRootR :: Handler RepHtml getRootR = do (res, form, enctype) <- runFormGet $ paramsFormlet Nothing
- utput <-
case res of FormMissing -> return "Please fill out the form to get a result." FormFailure _ -> return "Please correct the errors below." FormSuccess (Params min max single plural) -> do number <- liftIO $ randomRIO (min, max) let word = if number == 1 then single else plural return $ T.concat ["You got ", T.pack $ show number, " ", word] defaultLayout [$hamlet| <p>#{output} <form enctype="#{enctype}"> <table> ^{form} <tr> <td colspan="2"> <input type="submit" value="Randomize!"> |]
- - STOP
main = warpDebug 3001 Rand In lines 19-24, we set up a new datatype to hold all of the parameter information we need for this program. There's nothing special at all about this datatype, it's a POHD (Plain Old Haskell Datatype). Lines 26-32 are where we introduce the forms API in Yesod. First, notice the type signature: this function takes a Maybe Params. This allows us to either start with a blank form (Nothing) or initialize our form to some default value. This can be especially useful when creating an add/edit
- interface. The return type of the function is a Form; we'll deal more with that later. Also, since this pattern of taking a
Maybe value is so common, it has a synonym defined as Formlet (line 27). Let's skip down to line 29: we start off with <$>: this stresses the fact that we deal with forms using their Applicative instance. This allows us to check all fields in a form for validation errors. Lines 30-32 all start with <*>; you can see more information on the Applicative typeclass on the wikibook Applicative page. Continuing along line 29, we call intField. This is doing exactly what you expect: specifying that we want an integral value. "Minimum number" is the label displayed to the user, and the last part of the line gives the initial value
- f the field.
So how exactly is this initial value used? Well, if the user submits a value for the field, the initial value is ignored
- entirely. If there is not a user-submitted value (eg, this is the first time we are showing the user the form), if an initial
value is given, then that is put in the field. If no initial value is provided, the field starts off blank. Lines 30-32 should be easy enough to understand: they are almost identical to line 29. Returning back to line 28, we see a call to fieldsToTable. This function displays our form fields as a table, with one field per row. We have a few other options available, but this will most likely be the one you use most. Moving on to the handler function, on line 36 we call our paramsFormlet function with a Nothing argument: this means the form starts off blank. We pass this value to runFormGet, which binds our form to GET (aka, query- string) parameters. (Before you ask, yes, there is also runFormPost.) The return type of that function is a 3-tuple, containing:
- 1. The result of validating the form. We see below (lines 39-41) that there are three constructors for a
FormResult: FormMissing for when no data was submitted, FormFailure for validation errors, and FormSuccess when everything went OK.
OpenTopic | Basics | 37
- 2. The form itself, as a Widget. This value will include inline validation errors, the previously submitted values,
labels, etc. Notably, it does not include the <table> or <form> tags; you can look at the Hamlet template (lines 47-52) to see why that is.
- 3. The value for the enctype attribute of the form. Unless you have file submissions, this will be url-encoded.
Figure 1: Initial state of form Figure 2: After submitting the form Something not there to be noticed Did you notice that at no point did I specify the name attribute for any of those fields? There's actually a bit of magic at play here, so let's unravel it. First: Yesod can automatically provide unique names to forms, and by default it does
- that. Let's have a look at the generated code (newlines and tabbing added for convenience; Hamlet always spits out
condensed code): <form> <table> <tr> <td><label for="f3">Minimum number</label><div class="tooltip"></div></ td> <td><input id="f3" name="f2" type="number" required value=""></td> </tr> <tr> <td><label for="f5">Maximum number</label><div class="tooltip"></div></ td> <td><input id="f5" name="f4" type="number" required value=""></td> </tr> <tr> <td><label for="f7">Single word</label><div class="tooltip"></div></td> <td><input id="f7" name="f6" type="text" required value=""></td> </tr> <tr> <td><label for="f9">Plural word</label><div class="tooltip"></div></td> <td><input id="f9" name="f8" type="text" required value=""></td> </tr> <tr> <td colspan="2"><input type="submit" value="Randomize!"></td> </tr> </table> </form> We can see these f2, f3 and so on names and ids have been sprinkled everywhere. This can be incredibly convenient: we never have to worry about coming up with good, unique names. But what if we want to manually specify names and ids? And additionally, what's up with those tooltip divs? The answer to both questions is that our original code involved a little bit of a trick. If you look back at line 27, the first argument to intField is "Minimum number." But if you look at the API docs for intField, you'll see that the first argument is something called FormFieldSettings. To make life easier for you, Yesod defines an IsString instance for FormFieldSettings, and if you turn on OverloadedStrings, you can pretend that the first argument is just a string. But if you want to, you can supply a FormFieldSettings value directly. Case in point, we could replace line 29 with the following: <*> stringField FormFieldSettings { ffsLabel = "Single word" , ffsTooltip = "The singular version of the object, eg mouse versus mice" , ffsId = Just "single-word" , ffsName = Just "single-word" } (fmap singleWord mparams) advanced:
38 | OpenTopic | Basics You may have noticed that the minimum and maximum number input fields have a type="number" attribute. This is an example of Yesod including HTML 5 support by
- default. On some browsers (Chrome, for instance) this
field gets rendered with up/down arrows to control to value, plus it prevents non-numeric input. On browsers without any specific support, it gets rendered as a plain type="text" input. diveintohtml5 has a chapter on forms. Custom fields Yesod comes built in with a large number of fields, for dates, numbers, booleans, lists, etc. It also provides "maybe" variants for most of these, allowing blank values. However, occassionally you really will want to write your own, custom field. Plus, writing a custom field is a great way to see the internals, so let's hop to. There's two slightly clumsy things about our previous example: it doesn't give a validation error when the minimum number is greater than the maximum, and it would be nice to show both numbers on the same row in the table. Let's go ahead and fix both issues. Just a forewarning: this code looks complicated, but it's really more tedious than anything else. The point of the Yesod form library is to alleviate the monotonous tasks involved in web forms. Unfortunately, that tedious work still has to happen somewhere. {-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod import Yesod.Form.Core import System.Random import Control.Applicative import Safe import Data.Maybe import Control.Monad import Data.Text (Text) import qualified Data.Text as T data Rand = Rand type Handler = GHandler Rand Rand mkYesod "Rand" [$parseRoutes| / RootR GET |] instance Yesod Rand where approot _ = ""
- - START
data Params = Params { numberRange :: (Int, Int) , singleWord :: Text , pluralWord :: Text } paramsFormlet :: Formlet s m Params paramsFormlet mparams = fieldsToTable $ Params <$> rangeField (fmap numberRange mparams) <*> stringField "Single word" (fmap singleWord mparams) <*> stringField "Plural word" (fmap pluralWord mparams) rangeField :: Maybe (Int, Int) -> GForm s m [FieldInfo s m] (Int, Int)
- - same as rangeField :: Maybe (Int, Int) -> FormField s m (Int, Int)
- - same as rangeField :: FormletField s m (Int, Int)
rangeField initial = GForm $ do minId <- newFormIdent minName <- newFormIdent maxId <- newFormIdent maxName <- newFormIdent env <- askParams let res =
OpenTopic | Basics | 39 case env of [] -> FormMissing -- no data provided at all _ -> case (lookup minName env, lookup maxName env) of (Just minString, Just maxString) -> case (readMay $ T.unpack minString, readMay $ T.unpack maxString) of (Just min, Just max) -> if min > max then FormFailure ["Min is greater than max"] else FormSuccess (min, max) _ -> FormFailure ["Expecting two integers"] _ -> FormFailure ["Range is required"] let minValue = fromMaybe "" $ lookup minName env `mplus` fmap (T.pack . show . fst) initial let maxValue = fromMaybe "" $ lookup maxName env `mplus` fmap (T.pack . show . snd) initial let fi = FieldInfo { fiLabel = "Number range" , fiTooltip = "" , fiIdent = minId -- for attribute of the label , fiInput = [$hamlet| Between # <input id="#{minId}" name="#{minName}" type="number" value="#{minValue}"> and # <input id="#{maxId}" name="#{maxName}" type="number" value="#{maxValue}"> |] , fiErrors = case res of FormFailure [x] -> Just $ toHtml x _ -> Nothing , fiRequired = True } return (res, [fi], UrlEncoded)
- - STOP
getRootR :: Handler RepHtml getRootR = do (res, form, enctype) <- runFormGet $ paramsFormlet Nothing
- utput <-
case res of FormMissing -> return "Please fill out the form to get a result." FormFailure _ -> return "Please correct the errors below." FormSuccess (Params range single plural) -> do number <- liftIO $ randomRIO range let word = if number == 1 then single else plural return $ T.concat ["You got ", T.pack $ show number, " ", word] defaultLayout $ do addCassius [$cassius|input[type=number] width: 50px .errors color: red |] [$hamlet|\ <p>#{output} <form enctype="#{enctype}"> <table> \^{form} <tr> <td colspan="2"> <input type="submit" value="Randomize!"> |]
40 | OpenTopic | Basics main = warpDebug 3001 Rand On line 2 we can see the relevant change to the Params datatype. Technically speaking, we could have left the datatype the same as before, but that would have made the body of the paramsFormlet function more complicated; I leave that as an exercise to the reader. On line 9, we now call the rangeField function (which we are about to define). This function does not take a FormFieldSettings argument; instead, the label, ids and names are all explicitly defined in the rangeField function itself. Lines 13-15 give three equivalent type signatures for rangeField. Let's point out a few important pieces:
- FieldInfo is a datatype which allows us to restructure a form in different ways. For example, fieldsToTable is able
to convert this datatype into an HTML table. FieldInfo contains such information as the label, tooltip, HTML of the input area and validation errors. We create a value of it on lines 37-51.
- Similar to GHandler and GWidget, GForm is a generic form. This should give a hint to the meaning of the s
and m parameters: these are the sub site and master site, respectively. Usually, this is not important. Sometimes, however, we may want to do more fancy things such as loading a list of possible values from a database, in which case we will need to pay attention to those parameters.
- And as before, Maybe + Form = Formlet. Additionally, we have the word Field tacked on in lines 14 and 15, to
indicate that we have a FieldInfo. Lines 17-20 acquire our unique ids and names. On line 21, askParams acquires the parameters the user submitted. Since this form gets run eventually with runFormGet, this receives the query-string (GET) parameters; if we used runFormPost, it would receive the request body (POST) parameters. Lines 22-34 introduce the tedious part of form checking; we need to do the following:
- 1. If there are no form parameters at all, then assume the user didn't submit the form and return a FormMissing (line
24).
- 2. If either of the minName and maxName parameters are missing, return a FormFailure indicating the the range is
required (line 34).
- 3. If either of the parameters do not parse an integers, return a FormFailure (line 33).
- 4. Now that we know we have two integers, check if minimum is greater than maximum. If so, return a FormFailure
(line 31). Otherwise, we have a success (line 32). advanced: Note that technically we should check in step 2 if the user submitted an empty string and consider that the same as not submitting a parameter at all. To make things a little simpler in the example, we ignored this case, since the empty string will be caught by step 3 anyway. Lines 35 and 36 set up the String "value" attribute for the minimum and maximum fields. The code may look a little tricky, but we essentially do the following:
- 1. If the use submitted a value, use it.
- 2. Otherwise, use the initial value.
- 3. If there is no initial value, use a blank.
Next we create a FieldInfo value. The one really confusing piece is the value of fiIdent: why did I choose minId and not maxId? The value of fiIdent gets used when constructing the final HTML, as the value of the "for" attribute on the label tag. By setting the value to minId, it means that when a user clicks on the label, the browser will shift focus to the minimum field, which is probably what we want. The values for fiInput and fiErrors are, once again, tedious but straight-forward. We can now put both of our input fields together, and if we created any FormFailure above, we display it in the fiErrors. Line 52 returns our 3-tuple of form result, FieldInfo and encoding type. Figure 3: Valid entry
OpenTopic | Basics | 41 Figure 4: Invalid entry Automatic Javascript goodness After seeing that last example, you may be questioning whether forms are really worth it. Overall, I think that creating a new field is about as difficult as writing all of the code "from scratch", without the forms library. But once you have the fields, using them is much simpler than the raw approach. And most of the time, the built in fields are sufficient. Just to leave you with a good taste in your mouth, let's see an example where the combination of forms and widgets can give you beautiful forms very easily. {-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod import Yesod.Form.Jquery import Yesod.Form.Nic import Data.Time import Control.Applicative import Data.List import Data.Text (Text) import qualified Data.Text as T data Js = Js type Handler = GHandler Js Js mkYesod "Js" [$parseRoutes| / RootR GET /colors ColorsR GET |] instance Yesod Js where approot _ = ""
- - START
instance YesodJquery Js instance YesodNic Js data Survey = Survey { birthday :: Maybe Day , favoriteColor :: Text , aboutMe :: Html } surveyFormlet msurvey = fieldsToTable $ Survey <$> maybeJqueryDayField def "My birthday" (fmap birthday msurvey) <*> jqueryAutocompleteField ColorsR "Favorite color" (fmap favoriteColor msurvey) <*> nicHtmlField "About me" { ffsId = Just "about-me" } (fmap aboutMe msurvey) getRootR = do (res, form, enctype) <- runFormGet $ surveyFormlet Nothing let msurvey = case res of FormSuccess x -> Just x _ -> Nothing defaultLayout $ do addCassius [$cassius|#about-me width: 400px height: 300px |] [$hamlet| $maybe survey <- msurvey <h3>Previous Entries $maybe bday <- birthday survey <p>Born on #{show bday} <p>Favorite color: #{favoriteColor survey} #{aboutMe survey} <form enctype="#{enctype}">
42 | OpenTopic | Basics <table> ^{form} <tr> <td colspan="2"> <input type="submit"> |]
- - STOP
getColorsR = do term <- runFormGet' $ stringInput "term" jsonToRepJson $ jsonList $ map (jsonScalar . T.unpack) $ filter (T.isPrefixOf term) colors where colors = T.words "red orange yellow green blue purple black brown" main = warpDebug 3001 Js On lines 1 and 2, we declare two new instances: YesodJquery and YesodNic. These typeclasses allow us to specify where to get the relevant Javascript libraries and the attached CSS files. By default, they download them from CDNs. Line 3 starts our Survey datatype. On lines 9 through 14, we set up our surveyFormlet, very similar to our formlet from the previous two examples. The jqueryAutocompleteField function takes a route as its first argument, of where to get the autocomplete results. The rest of the example is fairly boilerplate. What's awesome about this example is that no where did we pull in the Javascript libraries: it was taken care of for us automatically. This is the huge advantage of widgets: we are able to package up some complete functionality in one module and use it with a single line of code in another. All of the field set up code was also handled for us automatically. Figure 5: jQuery date selection Figure 6: jQuery autocomplete Figure 7: NIC HTML Editor advanced: You may be screaming at your screen right now, "Isn't that an XSS vulnerability?" Normally, allowing a user to input arbitrary HTML to a page would be dangerous. However, thanks to the xss-sanitize package, all user input is validated and ensured to not have XSS attacks.
Summary
Forms are a complicated part of web programming, involving a lot of monotony. The form library in Yesod can turn this error-prone process into a nice, declarative experience. Teamed up with widgets, you can even create advanced Javascript-powered UIs without a sweat.
Sessions
As much as possible, RESTful applications should avoid storing state about an interaction with a client. However, it is sometimes unavoidable. Features like shopping carts are the classic example, but other more mundane interactions like proper login handling can be greatly enhanced by proper usage of sessions. This chapter will describe how Yesod stores session data, how you can access this data, and some special functions to help you make the most of sessions.
OpenTopic | Basics | 43
Clientsession
One of the earliest packages spun off from Yesod was clientsession. This package uses encryption and signatures to store data in a client-side cookie. The encryption prevents the user from tampering with the data, and the signature ensures that the session cannot be hijacked. It might sound like a bad idea from an efficiency standpoint to store data in a cookie: after all, this means that the data must be sent on every request. However, in practice, clientsession can be a great boon for performance.
- No server side database lookup is required to service a request.
- We can easily scale horizontally: each request contains all the information we need to send a response.
- Production sites should serve their static content from a separate domain name to avoid the overhead of
transmitting the session cookie for each request. Obviously, storing megabytes of information in the session will be a bad idea. But for that matter, most session implementations recommend against such practices. If you really need massive storage for a user, it is best to simply store a lookup key in the session, and put the actual data in a database.
Controlling sessions
There are three functions in the Yesod typeclass that control how sessions work. encryptKey returns the encryption key used. By default, it will take this from a local file, so that sessions can persist between database
- shutdowns. This file will be automatically created and filled with random data if it does not exist. And if you override
this function to return Nothing, sessions will be disabled. advanced: Why disable sessions? They do introduce a performance
- verhead. Under normal circumstances, this overhead
is minimal, especially compared to database access. However, when dealing with very basic tasks, the
- verhead can become noticeable. But be careful about
disabling sessions: this will also disable such features as CSRF (Cross-Site Request Forgery) protection. The next function is clientSessionDuration. This function simply gives the number of minutes that a session should be active. The default is 120 (2 hours). This value ends up affecting the session cookie in two ways: firstly, it determines the expiration date for the cookie
- itself. More importantly, however, the session expiration timestamp is encoded inside the session signature. When
Yesod decodes the signature, it checks if the date is in the past; if so, it ignores the session values. advanced: Every time Yesod sends a response to the client, it sends an updated session cookie with a new expire
- date. This way, even if you do not change the session
values themselves, a session will not time out if the user continues to browse your site. And this leads very nicely to the last function: sessionIpAddress. By default, Yesod also encodes the client's IP address inside the cookie to prevent session hijacking. In general, this is a good thing. However, some ISPs are known for putting their users behind proxies that rewrite their IP addresses, sometimes changing the source IP in the middle
- f the session. If this happens, and you have sessionIpAddress enabled, the user's session will be reset. Turning this
setting to false will allow a session to continue under such circumstances, at the cost of exposing a user to session hijacking.
44 | OpenTopic | Basics
Session Operations
Like most frameworks, sessions in Yesod are simple key-value stores. The base session API boils down to just three functions: lookupSession gets a value for a key (if available), setSession sets a value for a key, and deleteSession clears a value for a key.
- - START
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} import Yesod import Control.Applicative ((<$>), (<*>)) data Session = Session type Handler = GHandler Session Session mkYesod "Session" [$parseRoutes| / Root GET POST |] getRoot :: Handler RepHtml getRoot = do sess <- getSession hamletToRepHtml [$hamlet| <form method=post <input type=text name=key <input type=text name=val <input type=submit <h1>#{show sess} |] postRoot :: Handler () postRoot = do (key, mval) <- runFormPost' $ (,) <$> stringInput "key" <*> maybeStringInput "val" case mval of Nothing -> deleteSession key Just val -> setSession key val liftIO $ print (key, mval) redirect RedirectTemporary Root instance Yesod Session where approot _ = "" clientSessionDuration _ = 1 main = warpDebug 3000 Session
Messages
One usage of sessions previously alluded to is messages. They come to solve a common problem in web development: the user performs a POST request, the web app makes a change, and then the web app wants to simultaneously redirect the user to a new page and send the user a success message. Yesod provides a pair of functions to make this very easy: setMessage stores a value in the session, and getMessage both reads the value most recently put into the session, and clears the old value so it does not accidently get displayed twice. It is recommended to have a call to getMessage in defaultLayout so that any available message is shown to a user immediately, without having to remember to add getMessage calls to every handler.
Ultimate Destination
Not to be confused with a horror film, this concept is used internally in yesod-auth. Simply put, let's say a user requests a page that requires authentication. Clearly, you need to send them to the login page. A well-designed web app will then send them back to the first page they requested. That's what we call the ultimate destination.
OpenTopic | Basics | 45 redirectUltDest sends the user to the ultimate destination set in his/her session, clearing that value from the
- session. It takes a default destination as well, in case there is no destination set. For setting the session, there are three
variants: setUltDest sets the destination to the given type-safe URL, setUltDestString does the same with a text URL, and setUltDest' sets the destination to the currently requested URL.
Summary
The session API in Yesod is very simple. It provides a simple key-value store, and a few convenience functions built
- n top for common use cases. If used properly, with small payloads, sessions should be an unobtrusive part of your
web development.
Persistent
Forms deal with the boundary between the user and the application. Another boundary we need to deal with is between the application and the storage layer. Whether it be a SQL database, a YAML file, or a binary blob, odds are you have to work to get your storage layer to accept your application datatypes. Persistent is Yesod's answer to data storage- a type-safe universal data store interface for haskell. Haskell has many different database bindings available. However, most of these have little knowledge of a schema and therefore do not provide useful static guarantees and force database-dependent interfaces and data structures
- n the programmer. Haskellers have attempted a more revolutionary route of creating haskell specific data stores to
get around these flaws that allow one to easily store any haskell type. These options are great for certain use cases, but they constrain one to the storage techniques provided by the library, do not interface well with other languages, and the flexibility can also mean one must write reams of code for querying data. In contrast, Persistent allows us to choose among existing databases that are highly tuned for different data storage use cases, interoperate with other programming languages, and to use a safe and productive query interface. Persistent follows the guiding principles of type safety and concise, declarative syntax. Some other nice features are:
- Database-agnostic. While the most highly supported backends are Postgresql and SQLite, there is alpha support
for MongoDB.
- By being non-relational in nature, we simultaneously are able to support a wider number of storage layers and are
not constrained by some of the performance bottlenecks incurred through joins.
- A major source of frustration in dealing with SQL databases is changes to the schema. Persistent can
automatically perform database migrations.
Solving the boundary issue
Let's say you are storing information on people in a SQL database. Your table might look something like: CREATE TABLE Person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER) And if you are using a database like PostgreSQL, you can be guaranteed that the database will never store some arbitrary text in your age field. (The same cannot be said of SQLite, but let's forget about that for now.) To mirror this database table, you would likely create a Haskell datatype that looks something like: data Person = Person { personName :: String , personAge :: Int } It looks like everything is type safe: the database schema matches our Haskell datatypes, the database ensures that invalid data can never make it into our data store, and everything is generally awesome. Well, until:
- You want to pull data from the database, and the database layer gives you the data in an untyped format.
- You want to find everyone older than 32, and you accidently write "thirtytwo" in your SQL statement. Guess
what: that will compile just fine, and you won't find out you have a problem until runtime.
- You decide you want to do something as simple as find the first 10 people alphabetically. No problem... until you
make a typo in your SQL. Once again, you don't find out until runtime.
46 | OpenTopic | Basics In dynamic languages, the answers to these issues is unit testing. For everything that can go wrong, make sure you write a test case. But as I am sure you are aware by now, that doesn't jive well with the Yesod approach to things. We like to take advantage of Haskell's strong typing to save us wherever possible, and data storage is no exception. So the question remains: how can we use Haskell's type system to save the day? Types Like routing, there is nothing intrinsically difficult about type-safe data access. It just requires a lot of monotonous, error prone, boiler plate code. This is a perfect use case for some Template Haskell to generate this code for us
- automatically. To start off with, let's analyze some data types and type classes.
PersistValue is the basic building block of Persistent. It is a very simple datatype that can represent data that gets sent to and from a database. Its definition is: data PersistValue = PersistText Text | PersistByteString ByteString | PersistInt64 Int64 | PersistDouble Double | PersistBool Bool | PersistDay Day | PersistTimeOfDay TimeOfDay | PersistUTCTime UTCTime | PersistNull | PersistList [PersistValue] | PersistMap [(T.Text, PersistValue)] | PersistForeignKey ByteString -- ^ intended especially for MongoDB backend Each Persistent backend needs to know how to translate the relevant values into something the database can
- understand. However, it would be awkward do have to express all of our data simply in terms of these basic types.
The next layer is the PersistField typeclass, which defines how an arbitrary Haskell datatype can be marshaled to and from a PersistValue. A PersistField correlates to a column in a SQL database. In our person example above, name and age would be our Persistfields. To tie up the user side of the code, our last typeclass is PersistEntity. An instance of PersistEntity correlates with a table in a SQL database. This typeclass defines a number of functions and some associated types. Code Generation In order to ensure that the PersistEntity instances match up properly with your Haskell datatypes, Persistent takes responsibility for both. This is also good from a DRY (DRY) perspective: you only need to define your entities once. Let's see a quick example:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite mkPersist [$persist| Person name String age Int |]
- - STOP
main = return () We use a combination of Template Haskell and Quasi-Quotation (like when defining routes): persist is a quasi-quoter which converts a whitespace-sensitive syntax into a list of entity definitions. mkPersist takes that list of entities and declares:
- One Haskell datatype for each entity.
OpenTopic | Basics | 47
- A PersistEntity instance for each datatype defined.
Of course, the interesting part is how to use this datatype once it is defined. {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) mkPersist [$persist| Person name String age Int |]
- - START
main = withSqliteConn ":memory:" $ runSqlConn $ do michaelId <- insert $ Person "Michael" 26 michael <- get michaelId liftIO $ print michael
- - STOP
We start off with some standard database connection code. In this case, we used the single-connection functions. Persistent also comes built in with connection pool functions, which we will generally want to use in production. In this example, we have seen two functions: insert creates a new record in the database and returns its ID. Like everything else in Persistent, IDs are type safe. Each PersistEntity has an associated type called Key. So when you call insert $ Person "Michael" 25, it gives you a value back of type Key Person. Note: In order to make code a bit shorter, and to help with declaring routes and foreign keys (discussed later), every key gets a one-word type synonym created as well. In the case of person, it would be type PersonId = Key Person. The next function we see is get, which attempts to load a value from the database using a Key. In Persistent, you never need to worry that you are using the key from the wrong table: trying to load up a different entity (like House) using a Key Person will never compile. PersistBackend One last detail is left unexplained from the previous example: what are those withSqliteConn and runSqlConn functions doing, and what is that monad that our database actions are running in? All database actions need to occur within an instance of PersistBackend. As its name implies, every backend (PostgreSQL, SQLite, MongoDB) defines its own instance of PersistBackend. This is where all the translations from PersistValue to database-specific values occur, where SQL query generation happens, and so on. advanced: As you can imagine, even though PersistBackend provides a safe, well-typed interface to the outside world, there are a lot of database interactions that could go
- wrong. However, by testing this code automatically and
thoroughly in a central location, we can centralize our error-prone code into a single location and make sure it is as bug-free as possible. withSqliteConn creates a single connection to a database using its supplied connection string. For our test cases, we will use ":memory:", which simply uses an in-memory database. runSqlConn uses that connection to run the inner action, in this case, SqlPersist. Both SQLite and PostgreSQL share the same instance of PersistBackend. One important thing to note is that everything which occurs inside a single call to runSqlConn runs in a single
- transaction. This has two important implications:
48 | OpenTopic | Basics
- For many databases, committing a transaction can be a costly activity. By putting multiple steps into a single
transaction, you can speed up code dramatically.
- If an exception is thrown anywhere inside a single call to runSqlConn, all actions will be rolled back.
Migrations
I'm sorry to tell you, but so far I have lied to you a bit: the example from the previous section does not actually work. If you try to run it, you will get an error message about a missing table. For SQL databases, one of the major pains can be managing schema changes. Instead of leaving this to the user, Persistent steps in to help, but you have to ask it to help. Let's see what this looks like:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.TH import Database.Persist.Sqlite import Control.Monad.IO.Class (liftIO) mkPersist [$persist| Person name String age Int |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration $ migrate (undefined :: Person) -- this line added: that's it! michaelId <- insert $ Person "Michael" 26 michael <- get michaelId liftIO $ print michael
- - STOP
With this one little code change, Persistent will automatically create your Person table for you. This split between runMigration and migrate allows you to migrate multiple tables simultaneously. This works when dealing with just a few entities, but can quickly get tiresome once we are dealing with a dozen
- entities. Instead of repeating yourself, Persistent provides a helper function:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String age Int Car color String make String model String |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - STOP
mkMigrate is a Template Haskell function which creates a new function that will automatically call migrate on all entities defined in the persist block. The share function is just a little helper that passes the information from the persist block to each Template Haskell function and concatenates the results.
OpenTopic | Basics | 49 Persistent has very conservative rules about what it will do during a migration. It starts by loading up table information from the database, complete with all defined SQL datatypes. It then compares that against the entity definition given in the code. For simple cases, it will automatically alter the schema:
- The datatype of a field changed. However, the database may object to this modification if the data cannot be
translated.
- A field was added. However, if the field is not null, no default value is supplied (we'll discuss defaults later) and
there is already data in the database, the database will not allow this to happen.
- A field is converted from not null to null. In the opposite case, Persistent will attempt the conversion, contingent
upon the database's approval.
- A brand new entity is added.
However, there are a number of cases that Persistent will not handle:
- Field or entity renames: Persistent has no way of knowing that "name" has now been renamed to "fullName": all it
sees is an old field called name and a new field called fullName.
- Field removals: since this can result in data loss, Persistent by default will refuse to perform the action (you can
force the issue by using runMigrationUnsafe instead of runMigration, though it is not recommended). runMigration will print out the migrations it is running on stderr (you can bypass this by using runMigrationSilent). Whenever possible, it uses ALTER TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and therefore Persistent must resort to copying the data from one table to another. Finally, if instead of performing a migration, you just want Persistent to give you hints about what migrations are necessary, use the printMigration function. This function will print out the migrations which runMigration would perform for you. This may be useful for performing migrations that Persistent is not capable of, for adding arbitrary SQL to a migration, or just to log what migrations occurred. advanced: Although there is no official Persistent integration, there is a haskell package that can assist with running migrations called dbmigrations.
Attributes
So far, we have seen a very simple syntax for our persist blocks: a line for the name of our entities, and then an indented line for each field with two words: the name of the field and the datatype of the field. Persistent handles more than this: you can assign an arbitrary list of attributes after the first two words on a line. Let's say that we want to add two new fields to our Person entity: a favorite color (optional), and the timestamp of when he/she was added to the system. For entities already in the database, we want to just use the current date-time for that timestamp.
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String age Int color String Maybe created UTCTime default=now() |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - STOP
50 | OpenTopic | Basics Maybe is one of many built in, single word attributes. We will see many more below. The default attribute is backend specific, and uses whatever syntax is understood by the database. In this case, it uses the database's built-in CURRENT_TIMESTAMP function. Let's say we now want to add a field for favorite programming language:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String age Int color String Maybe created UTCTime default=now() language String default='Haskell' |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - STOP
Note: The default attribute has absolutely no impact
- n the Haskell code itself; you still need to fill in all
- values. This will only affect the database schema and
automatic migrations. We need to surround the string with single quotes so that the database can properly interpret it. Finally, Persistent can use double quotes for containing white space, so let's say we want to set someone's default home country to the El Salvador:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String age Int color String Maybe created UTCTime default=now() language String default='Haskell' country String "default='El Salvador'" |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - STOP
Associated Types
We saw above that each PersistEntity has a Key associated type to provide for type-safe lookups by numerical ID. But let's say we are not satisfied with looking up by ID: I want to get every person from my database with the name "Michael" who is older than 25. In SQL, there's no type-safe way to do this. In Persistent, we can do it easily.
- - START
OpenTopic | Basics | 51 {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time import Control.Monad.IO.Class (liftIO) share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String Eq age Int Gt |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll michaels <- selectList [ PersonNameEq "Michael" , PersonAgeGt 25 ] [] 0 0 -- we will explain these later, all in good time liftIO $ print michaels
- - STOP
The first thing to notice is that we have added some extra attributes to our persist block: Eq for name and Gt for age. There are six filtering attributes:
- Eq- equals
- Ne- not equals
- Lt- less than
- Le- less than or equals
- Gt- greater than
- Ge- greater than or equals
- In- equals any member of a list (like a SQL IN)
By adding these attribute, mkPersist automatically adds corresponding data constructors to the Filter associated
- type. In our case, this comes out to the equivalent of
data Filter Person = PersonNameEq String | PersonAgeGt Int Notice how the datatype of the field is encoded in the data constructor itself. This is the very heart of Persistent's type
- safety. It's impossible to type in PersonAgeGt "twenty-five", the compiler just won't have it.
Similarly, Persistent uses the Order associated type for sorting results. This comes with two attributes: Asc for sorting in ascending order, and Desc for descending order. So assuming we want to get all Michaels older than 25 in descending chronological order: {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time import Control.Monad.IO.Class (liftIO) share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String Eq age Int Gt Desc |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - START
michaels <- selectList
52 | OpenTopic | Basics [ PersonNameEq "Michael" , PersonAgeGt 25 ] [ PersonAgeDesc ] 0 0 -- we will explain these later, all in good time
- - STOP
liftIO $ print michaels Finally, let's say we have decided to rename everyone older than 25 and named Michael to Mike. For this, we use the Update associated type: {-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String Eq Update age Int Gt |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll
- - START
updateWhere [ PersonNameEq "Michael" , PersonAgeGt 25 ] [ PersonName "Mike" ]
- - STOP
There are a number of functions which take these associated types as parameters. As usual, the Haddock documentation is always the most authoritative source for a complete lists of API functions. Uniqueness The final associated type is is Unique. Its usage is slightly different than that of its siblings:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time import Control.Monad.IO.Class (liftIO) share [mkPersist, mkMigrate "migrateAll"] [$persist| Person firstName String lastName String age Int Gt Desc PersonName firstName lastName |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll insert $ Person "Michael" "Snoyman" 26 michael <- getBy $ PersonName "Michael" "Snoyman" liftIO $ print michael
- - STOP
OpenTopic | Basics | 53 To declare a unique combination of fields, we add an extra line to our declaration. Persistent knows that it is defining a unique constructor, since the line begins with a capital letter. Each following word must be a field in this entity. The main restriction on uniqueness is that it can only be applied non-null fields. The reason for this is that the SQL standard is ambiguous on how uniqueness should be applied to NULL (eg, is NULL=NULL true or false?). Besides that ambiguity, most SQL engines in fact implement rules which would be contrary to what the Haskell datatypes anticipate (eg, PostgreSQL says that NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).
Relations
Persistent allows references between your data types in a manner that is consistent with supporting non-SQL
- databases. We do this by embedding a Key value in the related entity. So if a person has many cars:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Control.Monad.IO.Class (liftIO) import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String Car
- wner PersonId Eq
name String |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll bruce <- insert $ Person "Bruce Wayne" insert $ Car bruce "Bat Mobile" insert $ Car bruce "Porsche"
- - this could go on a while
cars <- selectList [CarOwnerEq bruce] [] 0 0 liftIO $ print cars
- - STOP
advanced: You might be wondering what that PersonId is. Persistent automatically defines a type synonym type PersonId = Key Person for all of your entities. Using this technique, it's very easy to define one-to-many relationships. To define many-to-many relationships, we need a join entity, which has a one-to-many relationship with each of the original tables. It is also a good idea to use uniqueness constraints on these. For example, to model a situation where we want to track which people have shopped in which stores:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String Store name String
54 | OpenTopic | Basics PersonStore person PersonId store StoreId UniquePersonStore person store |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll bruce <- insert $ Person "Bruce Wayne" michael <- insert $ Person "Michael" target <- insert $ Store "Target" gucci <- insert $ Store "Gucci" sevenEleven <- insert $ Store "7-11" insert $ PersonStore bruce gucci insert $ PersonStore bruce sevenEleven insert $ PersonStore michael target insert $ PersonStore michael sevenEleven
- - STOP
Custom Fields
Occassionally, you will want to define a custom field to be used in your datastore. The most common case is an enumeration, such as employment status. For this, Persistent provides a helper Template Haskell function:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings #-} import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH import Data.Time data Employment = Employed | Unemployed | Retired deriving (Show, Read, Eq) derivePersistField "Employment" share [mkPersist, mkMigrate "migrateAll"] [$persist| Person name String employment Employment |] main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll insert $ Person "Bruce Wayne" Retired insert $ Person "Peter Parker" Unemployed insert $ Person "Michael" Employed
- - STOP
derivePersistField stores the data in the database using a string field, and performs marshaling using the Show and Read instances of the datatype. This may not be as efficient as storing via an integer, but it is much more future proof: even if you add extra constructors in the future, your data will still be valid.
Persistent: Raw SQL
The Persistent packages provides a type safe interface to data stores. It tries to be backend-agnostic, such as not relying on relational features of SQL. My experience has been you can easily perform 95% of what you need to do with the high-level interface. (In fact, most of my web apps use the high level interface exclusively.)
OpenTopic | Basics | 55 But occassionally you'll want to use a feature that's specific to a backend. One feature I've used in the past is full text
- search. In this case, we'll use the SQL "LIKE" operator, which is not modeled in Persistent. We'll get all people with
the last name "Snoyman" and print the records out. {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} import Database.Persist.Sqlite (withSqliteConn) import Database.Persist.TH (mkPersist, persist, share, mkMigrate) import Database.Persist.GenericSql (runSqlConn, runMigration, SqlPersist) import Database.Persist.GenericSql.Raw (withStmt) import Database.Persist.GenericSql.Internal (RowPopper) import Data.Text (Text) import Database.Persist import Control.Monad.IO.Class (liftIO) share [mkPersist, mkMigrate "migrateAll"] [persist| Person name Text |] main :: IO () main = withSqliteConn ":memory:" $ runSqlConn $ do runMigration migrateAll insert $ Person "Michael Snoyman" insert $ Person "Miriam Snoyman" insert $ Person "Eliezer Snoyman" insert $ Person "Gavriella Snoyman" insert $ Person "Greg Weber" insert $ Person "Rick Richardson"
- - Persistent does not provide the LIKE keyword, but we'd like to get the
- - whole Snoyman family...
let sql = "SELECT name FROM Person WHERE name LIKE '%Snoyman'" withStmt sql [] withPopper
- - A popper returns one row at a time. We loop over it until it returns
Nothing. withPopper :: RowPopper (SqlPersist IO) -> SqlPersist IO () withPopper popper = loop where loop = do mrow <- popper case mrow of Nothing -> return () Just row -> liftIO (print row) >> loop
FIXME Outline
- mkToForm
- Advanced: select enumerator interface
- raw SQL
- Join modules
- custom table/column names
56 | OpenTopic | Advanced
Advanced
RESTful Content
One of the stories from the early days of the web is how search engines wiped out entire websites. When dynamic web sites were still a new concept, developers didn't appreciate the difference between a GET and POST request. As a result, they created pages- accessed with the GET method- that would delete pages. When search engines started crawling these sites, they could wipe out all the content. If these web developers had followed the HTTP spec properly, this would not have happened. A GET request is supposed to cause no side effects (you know, like wiping out a site). Recently, there has been a move in web development called Representational State Transfer, also known as REST. This chapter describes the RESTful features in Yesod and how you can use them to create more robust web applications.
Request methods
In many web frameworks, you write one handler function per resource. In Yesod, the default is to have a separate handler function for each request method. The two most common request methods you will deal with in creating web sites are GET and POST. These are the most well-supported methods in HTML, since they are the only ones supported by web forms. However, when creating RESTful APIs, the other methods are very useful. Technically speaking, you can create whichever request methods you like, but it is strongly recommended to stick to the ones spelled out in the HTTP spec. The most common of these are: GET Read-only requests. Assuming no other changes
- ccur on the server, calling a GET request multiple
times should result in the same response, barring such things as "current time" or randomly assigned results. POST A general mutating request. A POST request should never be submitted twice by the user. A common example of this would be to transfer funds from one bank account to another. PUT Create a new resource on the server, or replace an existing one. This method is safe to be called multiple times. DELETE Just like it sounds: wipe out a resource on the
- server. Calling multiple times should be OK.
To a certain extent, this fits in very well with Haskell philosophy: a GET request is similar to a pure function, which cannot have side effects. Of course, in practice, your GET functions will probably perform IO, such as reading information from a database, logging user actions, and so on. See the routing and handlers chapter chapter for more information on the syntax of defining handler functions for each request method.
Representations
Let's say we have a Haskell datatype and value: data Person = Person { name :: String, age :: Int } michael = Person "Michael" 25
OpenTopic | Advanced | 57 We could represent that data as HTML: <table> <tr> <th>Name</th> <td>Michael</td> </tr> <tr> <th>Age</th> <td>25</td> </tr> </table>
- r we could represent it as JSON:
{"name":"Michael","age":25}
- r as XML:
<person> <name>Michael</name> <age>25</age> </person> Often times, web applications will use a different URL to get each of these representations; perhaps /person/ michael.html, /person/michael.json, etc. Yesod follows the RESTful principle of a single URL for each resource. So in Yesod, all of these would be accessed from /person/michael. Then the question becomes how do we determine which representation to serve. The answer is the HTTP Accept header: it gives a prioritized list of content types the client is expecting. Yesod will automatically determine which representation to serve based upon this header. Let's make that last sentence a bit more concrete with some code: class HasReps a where chooseRep :: a -> [ContentType] -> IO (ContentType, Content) The chooseRep function takes two arguments: the value we are getting representations for, and a list of content types that the client will accept. We determine this by reading the "Accept" request header. chooseRep returns a tuple containing the content type of our response and the actual content. This typeclass is the core of Yesod's RESTful approach to representations. Every handler function must return an instance of HasReps. Yesod provides a number of instances of HasReps out of the box. When we use defaultLayout, for example, the return type is RepHtml, which looks like: newtype RepHtml = RepHtml Content instance HasReps RepHtml where chooseRep (RepHtml content) _ = return ("text/html", content) What's interesting here is that we ignore entirely the list of expected content types. A number of the built in representations (RepHtml, RepPlain, RepJson, RepXml) in fact only support a single representation, and therefore what the client requests in the Accept header is irrelevant. RepHtmlJson An example to the contrary is RepHtmlJson, which provides either an HTML or JSON representation. This instance helps greatly in programming AJAX applications that degrade nicely. Here is an example that returns either HTML or JSON data, depending on what the client wants.
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} import Yesod data R = R mkYesod "R" [$parseRoutes| / RootR GET
58 | OpenTopic | Advanced /#String NameR GET |] instance Yesod R where approot _ = "" getRootR = defaultLayout $ do setTitle "Homepage" addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.4/ jquery.min.js" addJulius [$julius| $(function(){ $("a").click(function(){ jQuery.getJSON($(this).attr("href"), function(o){ $("div").text(o.name); }); return false; }); }); |] let names = words "Larry Moe Curly" addHamlet [$hamlet| <div id="results"> Your results will be placed here if you have Javascript enabled. <ul> $forall name <- names <li> <a href="@{NameR name}">#{name} |] getNameR name = do let widget = do setTitle $ string name addHamlet [$hamlet|Looks like you have Javascript off. Name: #{name}|] let json = jsonMap [("name", jsonScalar name)] defaultLayoutJson widget json main = warpDebug 4000 R
- - STOP
Our getRootR handler creates a page with three links and some Javascript which intercept clicks on the links and performs asynchronous requests. If the user has Javascript enabled, clicking on the link will cause a request to be sent with an "Accept" header of "application/json". In that case, getNameR will return the JSON representation defined on line 40. If the user disables Javascript, clicking on the link will send the user to the appropriate URL. A web browser places priority on an HTML representation of the data, and therefore the page defined on lines 36-38 will be returned. We can of course extend this to work with XML, Atom feeds, or even binary representations of the data. A fun exercise could be writing a web application that serves data simply using the default Show instances of datatypes, and then writing a web client that parses the results using the default Read instances. News Feeds A great, practical example of multiple representations if the yesod-newsfeed package. There are two major formats for news feeds on the web: RSS and Atom. They contain almost exactly the same information, but are just packaged up differently. The yesod-newsfeed package defines a Feed datatype which contains information like title, description, and last updated time. It then provides two separate sets of functions for displaying this data: one for RSS, one for Atom. They each define their own representation datatypes: newtype RepAtom = RepAtom Content instance HasReps RepAtom where
OpenTopic | Advanced | 59 chooseRep (RepAtom c) _ = return (typeAtom, c) newtype RepRss = RepRss Content instance HasReps RepRss where chooseRep (RepRss c) _ = return (typeRss, c) But there's a third module which defines another datatype: data RepAtomRss = RepAtomRss RepAtom RepRss instance HasReps RepAtomRss where chooseRep (RepAtomRss (RepAtom a) (RepRss r)) = chooseRep [ (typeAtom, a) , (typeRss, r) ] This datatype will automatically serve whichever representation the client prefers, defaulting to Atom. If a client connects that only understands RSS, assuming it provides the correct HTTP headers, Yesod will provide RSS output.
Other request headers
There are a great deal of other request headers available. Some of them simply affect the transfer of data between the server and client, and should not affect the application at all. For example, Accept-Encoding informs the server which compression schemes the client understands, and Host informs the server which virtual host to serve up. Other headers do affect the application, but are automatically read by Yesod. For example, the Accept-Language header specifies which human language (English, Spanish, German, Swiss-German) the client prefers. See the i18n chapter for details on how this header is used.
Stateless
I've saved this section for the last, not because it is less important, but rather because there are no specific features in Yesod to enforce this. HTTP is a stateless protocol: each request is to be seen as the beginning of a conversation. This means, for instance, it doesn't matter to the server if you requested five pages previously, it will treat your sixth request as if it's your first
- ne.
On the other hand, some features on websites won't work without some kind of state. For example, how can you implement a shopping cart without saving information about items in between requests? The solution to this is cookies, and built on top of this, sessions. We have a whole section addressing the sessions features in Yesod. However, I cannot stress enough that this should be used sparingly. Let me give you an example. There's a popular bug tracking system that I deal with on a daily basis which horribly abuses sessions. There's a little drop-down on every page to select the current project. Seems harmless, right? What that dropdown does is set the current project in your session. The result of all this is that clicking on the "view issues" link is entirely dependent on the last project you selected. There's no way to create a bookmark to your "Yesod" issues and a separate link for your "Hamlet" issues. The proper RESTful approach to this is to have one resource for all of the Yesod issues and a separate one for all the Hamlet issues. In Yesod, this is easily done with a route definition like: / ProjectsR GET /projects/#ProjectID ProjectIssuesR GET /issues/#IssueID IssueR GET Be nice to your users: proper stateless architecture means that basic features like bookmarks, permalinks and the back/forward button will always work.
Summary
Yesod adheres to the following tenets of REST:
- Use the correct request method.
- Each resource should have precisely one URL.
60 | OpenTopic | Advanced
- Allow multiple representations of data on the same URL.
- Inspect request headers to determine extra information about what the client wants.
This makes it easy to use Yesod not just for building websites, but for building APIs. In fact, using techniques such as RepHtmlJson, you can serve both a user-friendly, HTML page and a machine-friendly, JSON page from the same URL.
Authentication and Authorization
This chapter is just an outline. It will contain the sections:
- Authentication
- Authorization
- Auth stuff (isAuthorized, isWriteRequest, authRoute)
Note: Logout does not invalidate session.
Scaffolding and the Site Template
- How to scaffold: yesod executable
- File structure layout
- Some special features for static files (addStaticContent, urlRenderOverride)
Advanced Forms
This chapter is just an outline. It will contain the sections:
- GFormMonad
- Inner workings of GForm
- Meaning of the 5 type aliases
- Custom form layout
- Multiple rows (still have to figure out how to program that!)
Include information from blog post. http://www.haskell.org/pipermail/web-devel/2010/000507.html
Testing
FIXME This is just an outline. This chapter will mostly describe how to use wai-test.
Sending Email
Sorry, no information yet. This will mostly be about the mime-mail package.
Deploying your Webapp
I can't speak for others, but I personally prefer programming to system administration. But the fact is that, eventually, you need to serve your app somehow, and odds are that you'll need to be the one to set it up. There are some promising initiatives in the Haskell web community towards making deployment easier. In the future, we may even have a service that allows you to deploy your app with a single command. But we're not there yet. And even if we were, such a solution will never work for everyone. This chapter covers the different options you have for deployment, and gives some general recommendations on what you should choose in different situations.
OpenTopic | Advanced | 61
Warp
As we have mentioned before, Yesod is built on the Web Application Interface (WAI), allowing it to run on any WAI
- backend. At the time of writing, the following backends are available:
- Warp
- FastCGI
- SCGI
- CGI
- Webkit
- Development server
The last two are not intended for production deployments. Of the remaining four, all can be used for production deployment in theory. In practice, a CGI backend will likely be horribly inefficient, since a new process must be spawned for each connection. And SCGI is not nearly as well supported by frontend web server as Warp or FastCGI. So between the two remaining choices, Warp gets a very strong recommendation because:
- It is significantly faster.
- Like FastCGI, it can run behind a frontend server like Nginx, using reverse HTTP proxy.
- In addition, it is a fully capable server of its own accord, and can therefore be used without any frontend server.
But as fast as Warp is, it is still optimized as an application server, not a static file server. Therefore, for best performance, we recommend using Warp as your WAI backend, and to reverse proxy it behind Nginx, which is probably the fastest web server available today. Configuration In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on some unprivileged port (lets say 4321). You will then need to provide a nginx.conf file, such as: daemon off; # Don't run nginx in the background, good for monitoring apps events { worker_connections 4096; } http { server { listen 80; # Incoming port for Nginx server_name www.myserver.com; location / { proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app } } } You can add as many server blocks as you like. A common addition is to ensure users always access your pages with the www prefix on the domain name, ensuring the RESTful principle of canonical URLs. (You could just as easily do the opposite and always strip the www, just make sure that your choice is reflected in both the nginx config and the approot of your site.) In this case, we would add the block: server { listen 80; server_name myserver.com; rewrite ^/(.*) http://www.myserver.com/$1 permanent; } A highly recommended optimization is to server static files from a separate domain name, therefore bypassing the cookie transfer overhead. Assuming that our static files are stored in the static folder within our site folder, and the site folder is located at /home/michael/sites/mysite, this would look like: server { listen 80; server_name static.myserver.com;
62 | OpenTopic | Advanced root /home/michael/sites/mysite/static; expires max; # Take advantage of yesod-static's content hash query strings } In order for this to work, your site must properly rewrite static URLs to this alternate domain name. The scaffolded site is set up to make this fairly simple via the Settings.staticroot function and the definition of urlRenderOverride. However, if you just want to get the benefit of nginx's faster static file serving without dealing with separate domain names, you can instead modify your original server block like so: server { listen 80; # Incoming port for Nginx server_name www.myserver.com; location / { proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app } location /static { root /home/michael/sites/mysite; # Notice that we do *not* include / static expires max; } } Server Process Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of setup, where the web server automatically spawns the web application. With nginx, either for reverse proxying or FastCGI, this is not the case: you are responsible to run your own process. I strongly recommend a monitoring utility which will automatically restart your application in case it crashes. There are many great options out there, such as daemontools. To give a concrete example, here is an Upstart config file. The file must be placed in /etc/init/mysite.conf: description "My awesome Yesod application" start on runlevel [2345]; stop on runlevel [!2345]; respawn chdir /home/michael/sites/mysite exec /home/michael/sites/mysite/dist/build/mysite/mysite Once this is in place, bringing up your application is as simple as sudo start mysite.
FastCGI
Some people may prefer using FastCGI for deployment. In this case, you'll need to add an extra tool to the mix. FastCGI works by receiving new connection from a file descriptor. The C library assumes that this file descriptor will be 0 (standard input), so you need to use the spawn-fcgi program to bind your aplication's standard input to the correct socket. It can be very convenient to use Unix named sockets for this instead of binding to a port, especially when hosting multiple applications on a single host. A possible script to load up your app could be: spawn-fcgi \
- d /home/michael/sites/mysite \
- s /tmp/mysite.socket \
- n \
- M 511
- u michael
- - /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
You will also need to configure your frontend server to speak to your app over FastCGI. This is relatively painless in Nginx: server { listen 80; server_name www.myserver.com; location / {
OpenTopic | Advanced | 63 fastcgi_pass unix:/tmp/mysite.socket; } } That should look pretty familiar from above. The only last trick is that, with Nginx, you need to manually specify all
- f the FastCGI variables. It is recommended to store these in a separate file (say, fastcgi.conf) and then add include
fastcgi.conf; to the end of your http block. The contents of the file, to work with WAI, should be: fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param PATH_INFO $fastcgi_script_name; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name;
Desktop
Another nifty backend is wai-handler-webkit. This backend combines Warp and QtWebkit to create an executable that a user simply double-clicks. This can be a convenient way to provide an offline version of your application. One of the very nice conveniences of Yesod for this is that your templates are all compiled into the executable, and thus do not need to be distributed with your application. Static files do, however. Note: Earlier versions of Yesod allowed for embedding
- f static files in the executable as well. This is not a
particularly complicated feature to implement, and if there is demand, can be added back in the future.
CGI on Apache
CGI and FastCGI work almost identically on Apache, so it should be fairly straight-forward to port this configuration. You essentially need to accomplish two goals:
- 1. Get the server to serve your file as (Fast)CGI.
- 2. Rewrite all requests to your site to go through the (Fast)CGI executable.
Here is a configuration file for serving a blog application, with an executable named "bloggy.cgi", living in a subfolder named "blog" of the document root. This example was taken from an application living in the path /f5/ snoyman/public/blog. Options +ExecCGI AddHandler cgi-script .cgi Options +FollowSymlinks RewriteEngine On RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1] RewriteCond $1 !^bloggy.cgi RewriteCond $1 !^static/ RewriteRule ^(.*) bloggy.cgi/$1 [L] The first RewriteRule is to deal with subfolders. In particular, it redirects a request for /blog to /blog/. The first RewriteCond prevents directly requesting the executable, the second allows Apache to serve the static files, and the last line does the actual rewriting.
64 | OpenTopic | Advanced
FastCGI on lighttpd
For this example, I've left off some of the basic FastCGI settings like mime-types. I also have a more complex file in production that prepends "www." when absent and serves static files from a separate domain. However, this should serve to show the basics. Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to rewrite all requests to start with "/app", and then serve everything beginning with "/app" via the FastCGI executable. server.port = 3000 server.document-root = "/home/michael" server.modules = ("mod_fastcgi", "mod_rewrite") url.rewrite-once = ( "(.*)" => "/app/$1" ) fastcgi.server = ( "/app" => (( "socket" => "/tmp/test.fastcgi.socket", "check-local" => "disable", "bin-path" => "/home/michael/fastcgi", # full path to executable "min-procs" => 1, "max-procs" => 30, "idle-timeout" => 30 )) )
CGI on lighttpd
This is basically the same as the FastCGI version, but tells lighttpd to run a file ending in ".cgi" as a CGI executable. In this case, the file lives at "/home/michael/myapp.cgi". server.port = 3000 server.document-root = "/home/michael" server.modules = ("mod_cgi", "mod_rewrite") url.rewrite-once = ( "(.*)" => "/myapp.cgi/$1" ) cgi.assign = (".cgi" => "")
Internationalization
Yesod does not currently provide a prebuilt internationalization solution such as gettext. Instead, it provides some basic functionality for determining the user's language preference and leaves the rest to you. This chapter will give an example of how to use types to deal with i18n. We will additionally cover how to return responses in a character encoding besides UTF8. This chapter, however, has not yet been written. Please check back later.
Creating a Subsite
How many sites provide authentication systems? Or need to provide CRUD (CRUD) management of some objects? Or a blog? Or a wiki? The theme here is that many websites include common components that can be reused throughout multiple sites. However, it is often quite difficult to get code to be modular enough to be truly plug-and-play: a component will
OpenTopic | Advanced | 65 require hooks into the routing system, usually for multiple routes, and will need some way of sharing styling information with the master site. In Yesod, the solution is subsites. A subsite is a collection of routes and their handlers that can be easily inserted into a master site. By using type classes, it is easy to ensure that the master site provides certain capabilities, and to access the default site layout. And with type-safe URLs, it's easy to link from the master site to subsites.
Hello World
Writing subsites is a little bit tricky, involving a number of different types. Let's start off with a simple Hello World subsite:
- - START
{-# LANGUAGE QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-} {-# LANGUAGE TemplateHaskell, FlexibleInstances, OverloadedStrings #-} import Yesod
- - Subsites have foundations just like master sites.
data HelloSub = HelloSub
- - We have a familiar analogue from mkYesod, with just one extra parameter.
- - We'll discuss that later.
mkYesodSub "HelloSub" [] [$parseRoutes| / SubRootR GET |]
- - And we'll spell out the handler type signature.
getSubRootR :: Yesod master => GHandler HelloSub master RepHtml getSubRootR = defaultLayout [$hamlet|Welcome to the subsite!|]
- - And let's create a master site that calls it.
data Master = Master { getHelloSub :: HelloSub } mkYesod "Master" [$parseRoutes| / RootR GET /subsite SubsiteR HelloSub getHelloSub |] instance Yesod Master where approot _ = ""
- - Spelling out type signature again.
getRootR :: GHandler sub Master RepHtml -- could also replace sub with Master getRootR = defaultLayout [$hamlet| <h1>Welcome to the homepage <p> Feel free to visit the # <a href=@{SubsiteR SubRootR}>subsite \ as well. |] main = warpDebug 3000 $ Master HelloSub This very simple example actually shows most of the complications involved in creating a subsite. Like a normal Yesod application, everything in a subsite is centered around a foundation datatype, HelloSub in our case. We then use mkYesodSub, in much the same way that we use mkYesod, to create the route datatype and the dispatch/render
- functions. (We'll come back to that extra parameter in a second.)
What's interesting is the type signature of getSubRootR. Up until now, we have tried to ignore the GHandler datatype, or if we need to acknowledge its existence, pretend like the first two type arguments are always the same. Now we get to finally acknowledge the truth about this funny datatype.
66 | OpenTopic | Advanced A handler function always has two foundation types associated with it: the subsite and the master site. When you write a normal application, those two datatypes are the same. However, when you are working in a subsite, they will necessarily be different. So the type signature for getSubRootR uses HelloSub for the first argument and master for the second. The defaultLayout function is part of the Yesod typeclass. Therefore, in order to call it, the master type argument must be an instance of Yesod. The advantage of this approach is that any modifications to the master site's defaultLayout method will automatically be reflected in subsites. When we embed a subsite in our master site route definition, we need to specify four pieces of information: the route to use as the base of the subsite (in this case, /subsite), the constructor for the subsite routes (SubsiteR), the subsite foundation data type (HelloSub) and a function that takes a master foundation value and returns a subsite foundation value (getHelloSub). In the definition of getRootR, we can see how the route constructor gets used. In a sense, SubsiteR promotes any subsite route to a master site route, making it possible to safely link to it from any master site template.
Web Client Code
This chapter is not yet written. It covers writing applications which access web services. We will cover:
- http-enumerator
- gravatar
- reCaptcha
- Communicate with a Yesod service (JSON, cereal)
JSON Web Service
Let's create a very simple web service: it takes a JSON request and returns a JSON response. We're going to write the server in WAI/Warp, and the client in http-enumerator. We'll be using aeson for JSON parsing and rendering. Server WAI uses the enumerator package to handle streaming request bodies, and efficiently generates responses using blaze-builder. aeson uses attoparsec for parsing; by using attoparsec-enumerator we get easy interoperability with WAI. And aeson can encode JSON directly into a Builder. This plays out as: {-# LANGUAGE OverloadedStrings #-} import Network.Wai (Response (ResponseBuilder), Application) import Network.HTTP.Types (status200, status400) import Network.Wai.Handler.Warp (run) import Data.Aeson.Parser (json) import Data.Attoparsec.Enumerator (iterParser) import Control.Monad.IO.Class (liftIO) import Data.Aeson (Value (Object, String)) import Data.Aeson.Encode (fromValue) import Data.Enumerator (catchError, Iteratee) import Control.Exception (SomeException) import Data.ByteString (ByteString) import qualified Data.Map as Map import Data.Text (pack) main :: IO () main = run 3000 app app :: Application app _ = flip catchError invalidJson $ do value <- iterParser json newValue <- liftIO $ modValue value return $ ResponseBuilder status200 [("Content-Type", "application/json")]
OpenTopic | Advanced | 67 $ fromValue newValue invalidJson :: SomeException -> Iteratee ByteString IO Response invalidJson ex = return $ ResponseBuilder status400 [("Content-Type", "application/json")] $ fromValue $ Object $ Map.fromList [ ("message", String $ pack $ show ex) ]
- - Application-specific logic would go here.
modValue :: Value -> IO Value modValue = return Client http-enumerator was written as a comapnion to WAI. It too uses enumerator and blaze-builder pervasively, meaning we once again get easy interop with aeson. A few extra comments for those not familiar with http- enumerator:
- A Manager is present to keep track of open connections, so that multiple requests to the same server use the
same connection. You usually want to use the withManager function to create and clean up this Manager, since it is exception safe.
- We need to know the size of our request body, which can't be determined directly from a Builder. Instead, we
convert the Builder into a lazy ByteString and take the size from there.
- There are a number of different functions for initiating a request. We use http, which allows us to directly
access the data stream. There are other higher level functions (such as httpLbs) that let you ignore the issues of enumerators and get the entire body directly. {-# LANGUAGE OverloadedStrings #-} import Network.HTTP.Enumerator ( http, parseUrl, withManager, RequestBody (RequestBodyLBS) , requestBody ) import Data.Aeson (Value (Object, String)) import qualified Data.Map as Map import Data.Aeson.Parser (json) import Data.Attoparsec.Enumerator (iterParser) import Control.Monad.IO.Class (liftIO) import Data.Enumerator (run_) import Data.Aeson.Encode (fromValue) import Blaze.ByteString.Builder (toLazyByteString) main :: IO () main = withManager $ \manager -> do value <- makeValue
- - We need to know the size of the request body, so we convert to a
- - ByteString
let valueBS = toLazyByteString $ fromValue value req' <- parseUrl "http://localhost:3000/" let req = req' { requestBody = RequestBodyLBS valueBS } run_ $ flip (http req) manager $ \status headers -> do
- - Might want to ensure we have a 200 status code and Content-Type is
- - application/json. We skip that here.
resValue <- iterParser json liftIO $ handleResponse resValue
- - Application-specific function to make the request value
makeValue :: IO Value makeValue = return $ Object $ Map.fromList [ ("foo", String "bar") ]
- - Application-specific function to handle the response from the server
68 | OpenTopic | Advanced handleResponse :: Value -> IO () handleResponse = print
Low Level Tricks
This chapter, not yet written, will cover some more low-level details in Yesod. In particular, we'll touch on how to use the enumerator interface in WAI to do memory efficient file and database streaming without resorting to lazy IO.
OpenTopic | Appendices | 69
Appendices
Enumerator Package
One of the upcoming patterns in Haskell is enumerators. They are designed to solve the problems of producing, modifying and consuming streams of data. Enumerators are considered a bit intimidating, possibly because:
- There are multiple implementations, all with slightly different approaches.
- Some of the implementations (in my opinion) use incredibly confusing naming.
- The tutorials that get written usually don't directly target an existing implementation, and work more on building
up intuition than giving instructions on how to use the library. The Yesod framework uses enumerators behind the scenes in a number of places: in WAI/Warp for dealing with request and response bodies, in Persistent for receiving the results from a database query, and in various side packages like http-enumerator and xml-enumerator. We always use the enumerator package. That package is the topic of this chapter. This chapter is composed of three main sections, each dealing with one of the main concepts:
- We start with iteratees, which are consumers. They are fed data and do something with it.
- Next we cover enumerators which are producers. They feed data to an iteratee.
- Finally, we address enumeratees. These are pipes which are fed data from an enumerator and in turn feed data to
an iteratee. advanced: You may be wondering why we bother with this, when most of these problems can be addressed by lazy I/O. The basic problem with lazy I/O is its non-determinism. For more information, it's best to go to the original source on this topic: Oleg.
Iteratees
Intuition Let's say we want to write a function that sums the numbers in a list. Forgetting uninteresting details like space leaks, a perfectly good implementation could be: sum1 :: [Int] -> Int sum1 [] = 0 sum1 (x:xs) = x + sum1 xs But let's say that we don't have a list of numbers. Instead, the user is typing numbers on the command line, and hitting "q" when done. In other words, we have a function like: getNumber :: IO (Maybe Int) getNumber = do x <- readLine if x == "q" then return Nothing else return $ Just $ read x We could write our new sum function as: sum2 :: IO Int sum2 = do maybeNum <- getNumber case maybeNum of Nothing -> return 0
70 | OpenTopic | Appendices Just num -> do rest <- sum2 return $ num + rest It's fairly annoying to have to write two completely separate sum functions just because our data source changed. Ideally, we would like to generalize things a bit. Let's start by noticing a similarity between these two functions: they both only yield a value when they are informed that there are no more numbers. In the case of sum1, we check for an empty list; in sum2, we check for Nothing. Stream Datatype The first datatype defined in the enumerator package is: data Stream a = Chunks [a] | EOF The EOF constructor indicates that no more data is available. The Chunks constructor simply allows us to put multiple pieces of data together for efficiency. We could now rewrite sum2 to use this Stream datatype: getNumber2 :: IO (Stream Int) getNumber2 = do maybeNum <- getNumber -- using the original getNumber function case maybeNum of Nothing -> return EOF Just num -> return $ Chunks [num] sum3 :: IO Int sum3 = do stream <- getNumber2 case stream of EOF -> return 0 Chunks nums -> do let nums' = sum nums rest <- sum3 return $ nums' + rest Not that it's much better than sum2, but at least it shows how to use the Stream datatype. The problem here is that we still refer explicitly to the getNumber2 function, hard-coding the data source. One possible solution is to make the data source an argument to the sum function, ie: sum4 :: IO (Stream Int) -> IO Int sum4 getNum = do stream <- getNum case stream of EOF -> return 0 Chunks nums -> do let nums' = sum nums rest <- sum4 getNum return $ nums' + rest That's all well and good, but let's pretend we want to have two datasources to sum over: values the user enters on the command line, and some numbers we read over an HTTP connection, perhaps. The problem here is one of control: sum4 is running the show here by calling getNum. This is a pull data model. Enumerators have an inversion of control/push model, putting the enumerator in charge. This allows cool things like feeding in multiple data sources, and also makes it easier to write enumerators that properly deal with resource allocation. The Step datatype So we need a new datatype that will represent the state of our summing operation. We're going to allow our
- perations to be in one of three states:
- Waiting for more data.
- Already calculated a result.
- For convenience, we also have an error state. This isn't strictly necessary (it could be modeled by choosing an
EitherT kind of monad, for example), but it's simpler.
OpenTopic | Appendices | 71 As you could guess, these states will correspond to three constructors for the Step datatype. The error state is modeled by Error SomeException, building on top of Haskell's extensible exception system. The already calculated constructor is: Yield b (Stream a) Here, a is the input to our iteratee and b is the output. This constructor allows us to simultaneously produce a result and save any "leftover" input for another iteratee that may run after us. (This won't be the case with the sum function, which always consumes all its input, but we'll see some other examples that do not consume all output.) Now the question is how to represent the state of an iteratee that's waiting for more data. You might at first want to declare some datatype to represent the internal state and pass that around somehow. That's not how it works: instead, we simply use a function (very Haskell of us, right?): Continue (Stream a -> Iteratee a m b) Eureka! We've finally seen the Iteratee datatype! Actually, Iteratee is a very boring datatype that is only present to allow us to declare cool instances (eg, Monad) for our functions. Iteratee is defined as: newtype Iteratee a m b = Iteratee (m (Step a m b)) And the complete Step datatype is: data Step a m b = Error SomeException | Yield b (Stream a) | Continue (Stream a -> Iteratee a m b) This is important: Iteratee is just a newtype wrapper around a Step inside a monad. Just keep that in mind as you look at definitions in the enumerator package. So knowing this, we can think of the Continue constructor as: Continue (Stream a -> m (Step a m b)) That's much easier to approach: that function takes some input data and returns a new state of the iteratee. Let's see what our sum function would look like using this Step datatype: sum5 :: Monad m => Step Int m Int -- Int input, any monad, Int output sum5 = Continue $ go 0 -- a common pattern, you always start with a Continue where go :: Monad m => Int -> Stream Int -> Iteratee Int m Int
- - Add the new input to the running sum and create a new Continue
go runningSum (Chunks nums) = do let runningSum' = runningSum + sum nums
- - This next line is *ugly*, good thing there are some helper
- - functions to clean it up. More on that below.
Iteratee $ return $ Continue $ go runningSum'
- - Produce the final result
go runningSum EOF = Iteratee $ return $ Yield runningSum EOF Note: In order to run this code, you can use run_ $ enumList 8 [1..10] sum5. But this gets into some
- f the Enumerator black magic we won't discuss till later.
The first real line (Continue $ go 0) initializes our iteratee to its starting state. Just like every other sum function, we need to explicitly state that we are starting from 0 somewhere. The real workhorse is the go function. Notice how we are really passing the state of the iteratee around as the first argument to go: this is also a very common pattern in iteratees. We need to handle two different cases: when handed an EOF, the go function must Yield a value. (Well, it could also produce an Error value, but it definitely cannot Continue.) In that case, we simply yield the running sum and say there was no data left over. When we receive some input data via Chunks, we simply add it to the running sum and create a new Continue based on the same go function.
72 | OpenTopic | Appendices Now let's work on making that function a little bit prettier by using some built-in helper functions. The pattern Iteratee . return is common enough to warrant a helper function, namely: returnI :: Monad m => Step a m b -> Iteratee a m b returnI = Iteratee . return So for example, go runningSum EOF = Iteratee $ return $ Yield runningSum EOF becomes go runningSum EOF = returnI $ Yield runningSum EOF But even that is common enough to warrant a helper function: yield :: Monad m => b -> Stream a -> Iteratee a m b yield x chunk = returnI $ Yield x chunk so our line becomes go runningSum EOF = yield runningSum EOF Similarly, Iteratee $ return $ Continue $ go runningSum' becomes continue $ go runningSum' Monad instance for Iteratee This is all very nice: we now have an iteratee that can be fed numbers from any monad and sum them. It can even take input from different sources and sum them together. (By the way, I haven't actually shown you how to feed those numbers in: that is in part 2 about enumerators.) But let's be honest: sum5 is an ugly function. Isn't there something easier? In fact, there is. Remember how I said Iteratee really just existed to facilitate typeclass instances? This includes a monad instance. Feel free to look at the code to see how that instance is defined, but here we'll just look at how to use it: sum6 :: Monad m => Iteratee Int m Int sum6 = do maybeNum <- head -- not head from Prelude! case maybeNum of Nothing -> return 0 Just i -> do rest <- sum6 return $ i + rest That head function is not from Prelude, it's from the Data.Enumerator module. Its type signature is: head :: Monad m => Iteratee a m (Maybe a) which basically means give me the next piece of input if it's there. We'll look at this function in more depth in a bit. Go compare the code for sum6 with sum2: they are amazingly similar. You can often build up more complicated iteratees by using some simple iteratees and the Monad instance of Iteratee. Interleaved I/O Alright, let's look at a totally different problem. We want to be fed some strings and print them to the screen one line at a time. One approach would be to use lazy I/O: lazyIO :: IO () lazyIO = do s <- lines `fmap` getContents mapM_ putStrLn s
OpenTopic | Appendices | 73 But this has two drawbacks:
- It's tied down to a single input source, stdin. This could be worked around with an argument giving a datasource.
- But let's say the data source is some scarce resource (think: file handles on a very busy web server). We have no
guarantees with lazy I/O of when those file handles will be released. Let's look at how to write this in our new high-level monadic iteratee approach: interleaved :: MonadIO m => Iteratee String m () interleaved = do maybeLine <- head case maybeLine of Nothing -> return () Just line -> do liftIO $ putStrLn line interleaved The liftIO function comes from the transformers package, and simply promotes an action in the IO monad to any arbitrary MonadIO action. Notice how we don't really track any state with this iteratee: we don't care about its result,
- nly its side effects.
Implementing head As a last example, let's actually implement the head function. head' :: Monad m => Iteratee a m (Maybe a) head' = continue go where go (Chunks []) = continue go go (Chunks (x:xs)) = yield (Just x) (Chunks xs) go EOF = yield Nothing EOF Like our sum6 function, this also wraps an inner "go" function with a continue. However, we now have three clauses for our go function. The first handles the case of Chunks []. To quote the enumerator docs: (Chunks []) is used to indicate that a stream is still active, but currently has no available data. Iteratees should ignore empty chunks. The second clause handles the case where we are given some data. In this case, we yield the first element in the list, and return the rest as leftover data. The third clause handles the end of input by returning Nothing. Exercises
- Rewrite sum6 using liftFoldL'.
- Implement the consume function using first the high-level functions like head, and then using only low-level stuff.
- Write a modified version of consume that only keeps every other value, once again using high-level functions and
then low-level constructors. Summary Here's what I consider the most important things to glean from this tutorial:
- Iteratee is a simple wrapper around the Step datatype to allow for cool typeclass instances.
- Using the Monad instance of Iteratee can allow you to build up complicated iteratees from simpler ones.
- The three states an enumerator can be in are Continue (still processing data), Yield (a result is ready) and Error
(duh).
- Well behaved iteratees will never return a Continue after receiving an EOF.
Enumerators
Extracting a value So far, we've written a few iteratees, but we still don't know how to extract values from them. To start, let's remember that Iteratee is just a newtype wrapper around Step: newtype Iteratee a m b = Iteratee { runIteratee :: m (Step a m b) }
74 | OpenTopic | Appendices First we need to unwrap the Iteratee and deal with the Step value inside. Remember also that Step has three constructors: Continue, Yield and Error. We'll handle the Error constructor by returning our result in an Either. Yield already provides the data we're looking for. The tricky case is Continue: here, we have an iteratee that is still expecting more data. This is where the EOF constructor comes in handy: it's our little way to tell the iteratee to finish what it's doing and get on with things. If you remember from above, I said a well-behaving iteratee will never return a Continue after receiving an EOF; now we'll see why: extract :: Monad m => Iteratee a m b -> m (Either SomeException b) extract (Iteratee mstep) = do step <- mstep case step of Continue k -> do let Iteratee mstep' = k EOF step' <- mstep' case step' of Continue _ -> error "Misbehaving iteratee" Yield b _ -> return $ Right b Error e -> return $ Left e Yield b _ -> return $ Right b Error e -> return $ Left e Fortunately, you don't need to redefine this yourself: enumerator includes both a run and run_ function. Let's go ahead and use it on our sum6 function: main = run_ sum6 >>= print If you run this, the result will be 0. This emphasizes an important point: an iteratee is not just how to process incoming data, it is the state of the processing. In this case, we haven't done anything to change the initial state of sum6, so we still have the initial value of 0. To give an analogy: think of an iteratee as a machine. When you feed it data, you modify the internal state but you can't see any of those changes on the outside. When you are done feeding the data, you press a button and it spits out the result. If you don't feed in any data, your result is the initial state. Adding data Let's say that we actually want to sum some numbers. For example, the numbers 1 to 10. We need some way to feed that into our sum6 iteratee. In order to approach this, we'll once again need to unwrap our Iteratee and deal with the Step value directly. In our case, we know with certainty that the Step constructor we used is Continue, so it's safe to write our function as: sum7 :: Monad m => Iteratee Int m Int sum7 = Iteratee $ do Continue k <- runIteratee sum6 runIteratee $ k $ Chunks [1..10] But in general, we won't know what constructor will be lying in wait for us. We need to properly deal with Continue, Yield and Error. We've seen what to do with Continue: feed it the data. With Yield and Error, the right action in general is to do nothing, since we've already arrived at our final result (either a successful Yield or an Error). So the "proper" way to write the above function is: sum8 :: Monad m => Iteratee Int m Int sum8 = Iteratee $ do step <- runIteratee sum6 case step of Continue k -> runIteratee $ k $ Chunks [1..10] _ -> return step
OpenTopic | Appendices | 75 Enumerator type synonym What we've done with sum7 and sum8 is perform a transformation on the Iteratee. But we've done this in a very limited way: we've hard-coded in the original Iteratee function (sum6). We could just make this an argument to the function: sum9 :: Monad m => Iteratee Int m Int -> Iteratee Int m Int sum9 orig = Iteratee $ do step <- runIteratee orig case step of Continue k -> runIteratee $ k $ Chunks [1..10] _ -> return step But since we always just want to unwrap the Iteratee value anyway, it turns out that it's more natural to make the argument of type Step, ie: sum10 :: Monad m => Step Int m Int -> Iteratee Int m Int sum10 (Continue k) = k $ Chunks [1..10] sum10 step = returnI step This type signature (take a Step, return an Iteratee) turns out to be very common: type Enumerator a m b = Step a m b -> Iteratee a m b Meaning sum10's type signature could also be expressed as: sum10 :: Monad m => Enumerator Int m Int Of course, we need some helper function to connect an Enumerator and an Iteratee: applyEnum :: Monad m => Enumerator a m b -> Iteratee a m b -> Iteratee a m b applyEnum enum iter = Iteratee $ do step <- runIteratee iter runIteratee $ enum step Let me repeat the intuition here: the Enumerator is transforming the Iteratee from its initial state to a new state by feeding it more data. In order to use this function, we could write: run_ (applyEnum sum10 sum6) >>= print This results in 55, exactly as we'd expect. But now we can see one of the benefits of enumerators: we can use multiple data sources. Let's say we have another enumerator: sum11 :: Monad m => Enumerator Int m Int sum11 (Continue k) = k $ Chunks [11..20] sum11 step = returnI step Then we could simply apply both enumerators: run_ (applyEnum sum11 $ applyEnum sum10 sum6) >>= print And we would get the result 210. (Yes, (1 + 20) * 10 = 210.) But don't worry, you don't need to write this applyEnum function yourself: enumerator provides a $$ operator which does the same thing. Its type signature is a bit scarier, since it's a generalization of applyEnum, but it works the same, and even makes code more readable: run_ (sum11 $$ sum10 $$ sum6) >>= print $$ is a synonym for ==<<, which is simply flip >>==. I find $$ the most readable, but YMMV (YMMV). Some built-in enumerators Of course, writing a whole function just to pass some numbers to our sum function seems a bit tedious. We could easily make the list an argument to the function: sum12 :: Monad m => [Int] -> Enumerator Int m Int sum12 nums (Continue k) = k $ Chunks nums sum12 _ step = returnI step
76 | OpenTopic | Appendices But now there's not even anything Int-specific in our function. We could easily generalize this to: genericSum12 :: Monad m => [a] -> Enumerator a m b genericSum12 nums (Continue k) = k $ Chunks nums genericSum12 _ step = returnI step And in fact, enumerator comes built in with the enumList function which does this. enumList also takes an Integer argument to indicate the maximum number of elements to stick in a chunk. For example, we could write: run_ (enumList 5 [1..30] $$ sum6) >>= print (That produces 465 if you're counting.) The first argument to enumList should never affect the result, though it may have some performance impact. Data.Enumerator includes two other enumerators: enumEOF simply passes an EOF to the iteratee. concatEnums is slightly more interesting; it combines multiple enumerators together. For example: run_ (concatEnums [ enumList 1 [1..10] , enumList 1 [11..20] , enumList 1 [21..30] ] $$ sum6) >>= print This also produces 465. Some non-pure input Enumerators are much more interesting when they aren't simply dealing with pure values. In the first part of this tutorial, we gave the example of the user entering numbers on the command line: getNumber :: IO (Maybe Int) getNumber = do x <- getLine if x == "q" then return Nothing else return $ Just $ read x sum2 :: IO Int sum2 = do maybeNum <- getNumber case maybeNum of Nothing -> return 0 Just num -> do rest <- sum2 return $ num + rest We referred to this as the pull-model: sum2 pulled each value from getNumber. Let's see if we can rewrite getNumber to be a pusher instead of a pullee. getNumberEnum :: MonadIO m => Enumerator Int m b getNumberEnum (Continue k) = do x <- liftIO getLine if x == "q" then continue k else k (Chunks [read x]) >>== getNumberEnum getNumberEnum step = returnI step First, notice that we check which constructor was passed, and only perform any actions if it was Continue. If it was Continue, we get the line of input from the user. If the line is "q" (our indication to stop feeding in values), we do
- nothing. You might have thought that we should pass an EOF. But if we did that, we'd be preventing other data from
being sent to this iteratee. Instead, we simply return the original Step value.
OpenTopic | Appendices | 77 If the line was not "q", we convert it to an Int via read, create a Stream value with the Chunks datatype, and pass it to
- k. (If we wanted to do things properly, we'd check if x is really an Int and use the Error constructor; I leave that as an
exercise to the reader.) At this point, let's look at type signatures: k (Chunks [read x]) :: Iteratee Int m b If we simply left off the rest of the line, our program would typecheck. However, it would only ever read one value from the command line; the >>== getNumberEnum causes our enumerator to loop. One last thing to note about our function: notice the b in our type signature. getNumberEnum :: MonadIO m => Enumerator Int m b This is saying that our Enumerator can feed Ints to any Iteratee accepting Ints, and it doesn't matter what the final output type will be. This is in general the way enumerators work. This allows us to create drastically different iteratees that work with the same enumerators: intsToStrings :: (Show a, Monad m) => Iteratee a m String intsToStrings = (unlines . map show) `fmap` consume And then both of these lines work: run_ (getNumberEnum $$ sum6) >>= print run_ (getNumberEnum $$ intsToStrings) >>= print Exercises
- 1. Write an enumerator that reads lines from stdin (as Strings). Make sure it works with this iteratee:
printStrings :: Iteratee String IO () printStrings = do mstring <- head case mstring of Nothing -> return () Just string -> do liftIO $ putStrLn string printStrings
- 2. Write an enumerator that does the same as above with words (ie, delimit on any whitespace). It should work with
the same Iteratee as above.
- 3. Do proper error handling in the getNumberEnum function above when the string is not a proper integer.
- 4. Modify getNumberEnum to pull its input from a file instead of stdin.
- 5. Use your modified getNumberEnum to sum up the values in two different files.
Summary
- An enumerator is a step transformer: it feeds data into an iteratee to produce a new iteratee with an updated state.
- Multiple enumerators can be fed into a single iteratee, and we finally use the run and run_ functions to extract
results.
- We can use the $$, >>== and ==<< operators to apply an enumerator to an iteratee.
- When writing an enumerator, we only feed data to an iteratee in the Continue state; Yield and Error already
represent final values.
Enumeratees
Generalizing getNumberEnum Earlier, we created a getNumberEnum function with a type signature: getNumberEnum :: MonadIO m => Enumerator Int m b If you don't remember, this means getNumberEnum produces a stream of Ints. In particular, our getNumberEnum function read lines from stdin, converted them to ints and fed them into an iteratee. It stopped reading lines when it saw a "q". But this functionality seems like it could be useful outside the realm of Ints. We may like to deal with the original Strings, for example, or Bools, or a bunch of other things. We could easily define a more generalized function which simply doesn't do the String to Int conversion: lineEnum :: MonadIO m => Enumerator String m b lineEnum (Continue k) = do x <- liftIO getLine
78 | OpenTopic | Appendices if x == "q" then continue k else k (Chunks [x]) >>== lineEnum lineEnum step = returnI step Cool, let's plug this into our sumIter function (I've renamed the sum6 function from the previous two parts): lineEnum $$ sumIter Actually, that doesn't type check: lineEnum produces Strings, and sumIter takes Ints. We need to modify one of them somehow. sumIterString :: Monad m => Iteratee String m Int sumIterString = Iteratee $ do innerStep <- runIteratee sumIter return $ go innerStep where go :: Monad m => Step Int m Int -> Step String m Int go (Yield res _) = Yield res EOF go (Error err) = Error err go (Continue k) = Continue $ \strings -> Iteratee $ do let ints = fmap read strings :: Stream Int step <- runIteratee $ k ints return $ go step What we've done here is wrap around the original iteratee. As usual, we first need to unwrap the Iteratee constructor and the monad to get at the heart of the Step value. Once we have that innerStep value, we pass it to the go function, which simply transforms that values in the Stream value from Strings to Ints. Even more general Of course, it would be nice if we could apply this transformation to *any* iteratee. To start with, let's just pass the inner iteratee and the mapping function as parameters. mapIter :: Monad m => (aOut -> aIn) -> Iteratee aIn m b -> Iteratee aOut m b mapIter f innerIter = Iteratee $ do innerStep <- runIteratee innerIter return $ go innerStep where go (Yield res _) = Yield res EOF go (Error err) = Error err go (Continue k) = Continue $ \strings -> Iteratee $ do let ints = fmap f strings step <- runIteratee $ k ints return $ go step We could call this like: run_ (lineEnum $$ mapIter read sumIter) >>= print Nothing much to see here, it's basically identical to the previous version. What's funny is that enumerator comes built in with a map function to do just this, but it has a significantly different type signature: map :: Monad m => (ao -> ai) -> Enumeratee ao ai m b since: type Enumeratee aOut aIn m b = Step aIn m b -> Iteratee aOut m (Step aIn m b) that's equivalent to: map :: Monad m => (aOut -> aIn) -> Step aIn m b -> Iteratee aOut m (Step aIn m b) What's with all this extra complication in type signature? Well, it's not necessary for map itself, but it is necessary for a whole bunch of other similar functions. But let's focus on this map for a second so we don't get lost: the first argument is the same old mapping function we had before. The second argument is a Step value. This isn't really so
OpenTopic | Appendices | 79 surprising: in our mapIter, we took an Iteratee with the same parameters, and we internally just unwrapped it to a Step. But what's happening with that return value? Remembering the meanings for all these datatypes, it's an Iteratee which will be fed a stream of aOuts and return a Step (aka, a new iteratee, right?). This kind of makes intuitive sense: we've introduced a middle man which accepts input from one source and transforms a Step to a newer state. But now perhaps the trickiest part of the whole thing: how do we actually use this map function? It turns out that an Enumeratee is close enough in type signature to an Enumerator that we can just do: map read $$ sumIter But the type signature on that turns out to be a little bit weird: Iteratee String m (Step Int m Int) Remembering that an Iteratee is just a wrapped up Step, what we've got here is an iteratee that takes Strings and returns an Iteratee, which in turn takes Ints and produces an Int. Having this fancy result allows us to do one of our great tricks with iteratees: plug in data from multiple sources. For example, we could plug some Strings into this whole ugly thing, run it, get a new iteratee which takes Ints, feed that some Ints and get an Int result. (If all that went over your head, don't worry. I won't be talking about that kind of stuff any more.) But often times, we don't need all of that power. We just want to stick our enumeratee onto our iteratee and get a new iteratee. In our case, we want to attach our map onto the sumIter to produce a new iteratee that takes Strings and returns Ints. In order to do that, we need a function like this: unnest :: Monad m => Iteratee String m (Step Int m Int) -> Iteratee String m Int unnest outer = do -- using the Monad instance of Iteratee inner <- outer -- inner :: Step Int m Int go inner where go (Error e) = throwError e go (Yield x _) = yield x EOF go (Continue k) = k EOF >>== go We can then run our unholy mess with: run_ (lineEnum $$ unnest $ map read $$ sumIter) >>= print And actually, the unnest function is available in Data.Enumerator, and it's called joinI. So we should really write: run_ (lineEnum $$ joinI $ map read $$ sumIter) >>= print Skipping Let's write a slightly more interesting enumeratee: this one skips every other input value. skip :: Monad m => Enumeratee a a m b skip (Continue k) = do x <- head _ <- head -- the one we're skipping case x of Nothing -> return $ Continue k Just y -> do newStep <- lift $ runIteratee $ k $ Chunks [y] skip newStep skip step = return step What's interesting about the approach here is how similar it looks to an Enumerator. We're doing a lot of the same things: checking if the Step value is a Continue; if it's not, then simply return it. Then we capitalize on the Iteratee Monad instance, using the head function to pop two values out of the stream. If there's no more data, we return the
- riginal Continue value: just like with an Enumerator, we don't give an EOF so that we can feed more data into the
iteratee later. If there is data, we pass it off to the iteratee, get our new step value and then loop.
80 | OpenTopic | Appendices And what's cool about enumeratees is we can chain these all together: run_ (lineEnum $$ joinI $ skip $$ joinI $ map read $$ sumIter) >>= print Here, we read lines, skip every other input, convert the Strings to Ints and sum them. Real life examples: http-enumerator package I started working on these tutorials as I was working on the http-enumerator package. I think the usage of enumeratees there is a great explanation of the benefits they can offer in real life. There are three different ways the response body can be broken up:
- Chunked encoding. In this case, the web server gives a hex string specifying the length of the next chunk and then
that chunk. At the end, it sends a 0 to indicate the end of that response.
- Content length. Here, the web server sends a header before any of the body is sent specifying the total length of
the body.
- Nothing at all. In this case, the response body lasts until an end-of-file.
In addition, the body may or may not be GZIP compressed. We end up with the following enumeratees, each with type signature Enumeratee ByteString ByteString m b: chunkedEncoding, contentLength and ungzip. We then get to do something akin to: let parseBody x = if ("transfer-encoding", "chunked") `elem` responseHeaders then joinI $ chunkedEncoding $$ x else case mlen of Just len -> joinI $ contentLength len $$ x Nothing -> x -- no enumeratee applied at all let decompress x = if ("content-encoding", "gzip") `elem` responseHeaders then joinI $ ungzip $$ x else x run_ $ socketEnumerator $$ parseBody $ decompress $ bodyIteratee We create a chain: the data from the server is fed into the parseBody function. In the case of chunked encoding, the data is processed appropriately and then headers are filtered out. If we are dealing with content length, then only the specified number of bytes are read. And in the case of neither of those, parseBody is a no-op. Whatever the case may be, the raw response body is then fed into decompress. If the body is GZIPed, then ungzip inflates it, otherwise decompress is a no-op. Finally, the parsed and inflated data is fed into the user-supplied bodyIteratee function. The user remains blissfully unaware of any steps the data took to get to him/her. Exercises
- 1. Write an enumeratee which takes hex chars (eg, "DEADBEEF") to Word8s. Its type signature should be
Enumeratee Char Word8 m b.
- 2. Write the opposite enumeratee, eg Enumeratee Word8 Char m b.
- 3. Create a quickcheck property that ensures that these two functions work correctly.
Summary
- Enumeratees are the pipes connecting enumerators to iteratees.
- The strange type signature of an Enumeratee hides a lot of possible power. Especially notice how similar their
type signatures are to Enumerators.
- You can merge an Enumeratee into an Iteratee with joinI $ enumeratee $$ iteratee.
- Don't forget that you can use the Monad instance of Iteratee when creating your own enumeratees.
- You can always compose multiple enumeratees together, such as in http-enumerator.
Web Application Interface
It is a problem almost every language used for web development has dealt with: the low level interface between the web server and the application. The earliest example of a solution is the venerable and battle-worn CGI (CGI), providing a language-agnostic interface using only standard input, standard output and environment variables.
OpenTopic | Appendices | 81 Back when Perl was becoming the de facto web programming language, a major shortcoming of CGI became apparent: the process needed to be started anew for each request. When dealing with an interpretted language and application requiring database connection, this overhead became unbearable. FastCGI (and later SCGI) arose as a successor to CGI, but it seems that much of the programming world went in a different direction. Each language began creating its own standard for interfacing with servers. mod_perl. mod_python. mod_php. mod_ruby. Within the same language, multiple interfaces arose. In some cases, we even had interfaces on top of
- interfaces. And all of this led to much duplicated effort: a Python application designed to work with FastCGI wouldn't
work with mod_python; mod_python only exists for certain webservers; and this programming language specific extensions need to be written for each programming language. Haskell has its own history. We originally had the cgi package, which provided a monadic interface. The fastcgi package then provided the same interface. Meanwhile, it seemed that the majority of Haskell web development focused on the standalone server. The problem is that each server comes with its own interface, meaning that you need to target a specific backend. This means that it is impossible to share common features, like GZIP encoding, development servers, and testing frameworks. WAI attempts to solve this, by providing a generic and efficient interface between web servers and applications. Any handler supporting the interface can serve any WAI application, while any application using the interface can run on any handler. At the time of writing, there are various backends, including Warp, FastCGI, and development server. wai-extra provides many common middlewares like GZIP, JSON-P and virtual hosting. wai-test makes it easy to write unit tests, and wai-handler-devel lets you develop your applications without worrying about stopping to compile. Yesod targets WAI, and Happstack is in the process of converting over as well. It's also used by some applications that skip the framework entirely, including the new Hoogle.
The Interface
The interface itself is very straight-forward: an application takes a request and returns a response. A response is an HTTP status, a list of headers and a response body. A request contains various information: the requested path, query string, request body, HTTP version, and so on. Response Body Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you can create large values without exhausting memory. Using lazy I/O, you can do such tricks as having a value which represents the entire contents of a file, yet only occupies a small memory footprint. In theory, a lazy bytestring is the only representation necessary for a response body. In practice, while lazy byte strings are wonderful for generating "pure" values, the lazy I/O necessary to read a file introduces some non-determinism into our programs. When serving thousands of small files a second, the limiting factor is not memory, but file handles. Using lazy I/O, file handles may not be freed immediately, leading to resource
- exhaustion. To deal with this, WAI uses enumerators.
Enumerators are really a simple concept with a lot of complications surrounding them. Most basically, an enumerator is a data producer, that hands chunks of data one at a time to an iteratee, which is a data consumer. In the case of WAI, the request body would be an enumerator which would produce data by reading it from a file. The iteratee would be the server, which would send these chunks of data to the client. There are two further optimizations: many systems provide a sendfile system call, which sends a file directly to a socket, bypassing a lot of the memory copying inherent in more general I/O system calls. Additionally, there is a datatype in Haskell called Builder which allows efficient copying of bytes into buffers. The WAI response body therefore has three constructors: one for pure builders (ResponseBuilder), one for enumerators of builders (ResponseEnum) and one for files (ResponseFile).
82 | OpenTopic | Appendices Request Body In order to avoid the need to load the entire request body into memory, we use enumerators here as well. Since the purpose of these values are for reading (not writing), we use ByteStrings in place of Builders. This is all contained in the type signature of an Application: type Application = Request -> Iteratee ByteString IO Response This states that an application is a function, which takes a Request value and returns an action. This action consumes a stream of ByteStrings (the request body) and produces a Response. The request body could in theory contain any type of data, but the most common are URL encoded and multipart form data. The wai-extra package contains built-in support for parsing these in a memory-efficient manner.
Hello World
To demonstrate the simplicity of WAI, let's look at a hello world example. In this example, we're going to use the OverloadedStrings language extension to avoid explicitly packing string values into bytestrings. 1 {-# LANGUAGE OverloadedStrings #-} 2 import Network.Wai 3 import Network.HTTP.Types (statusOK) 4 import Network.Wai.Handler.Warp (run) 5 6 application _ = return $ 7 responseLBS statusOK [("Content-Type", "text/plain")] "Hello World" 8 9 main = run 3000 application Lines 2 through 4 perform our imports. Warp is provided by the warp package, and is the premiere WAI backend. WAI is also built on top of the http-types package, which provides a number of datatypes and convenience values, including statusOK. First we define our application. Since we don't care about the specific request parameters, we ignore the argument to the function. For any request, we are returning a response with status code 200 ("OK"), and text/plain content type and a body containing the words "Hello World". Pretty straight-forward.
Middleware
In addition to allowing our applications to run on multiple backends without code changes, the WAI allows us another benefits: middleware. Middleware is essentially an application transformer, taking one application and returning another one. Middlewares can be used to provide lots of services: cleaning up URLs, authentication, caching, JSON-P requests. But perhaps the most useful and most intuitive middleware is gzip compression. The middleware works very simply: it parses the request headers to determine if a client supports compression, and if so compresses the response body and adds the appropriate response header. The great thing about middlewares is that they are unobtrusive. Let's see how we would apply the gzip middleware to
- ur hello world application.
1 {-# LANGUAGE OverloadedStrings #-} 2 import Network.Wai 3 import Network.Wai.Handler.Warp (run) 4 import Network.Wai.Middleware.Gzip (gzip) 5 import Network.HTTP.Types (statusOK) 6 7 application _ = return $ responseLBS statusOK [("Content-Type", "text/ plain")] 8 "Hello World" 9 10 main = run 3000 $ gzip application
OpenTopic | Appendices | 83 We added an import line to actually have access to the middleware, and then simply applied gzip to our
- application. You can also chain together multiple middlewares: a line such as gzip False $ jsonp $
- thermiddleware $ myapplication is perfectly valid. One word of warning: the order the middleware is
applied can be important. For example, jsonp needs to work on uncompressed data, so if you apply it after you apply gzip, you'll have trouble.
Blog posts that should be chapters
Following is a list of blog posts that should ultimately have their content merged in with the rest of the book. Unfortunately, I just haven't had time to do so yet.
- The Magic of Yesod, part 2 December 25, 2010 Continues the previous blog post, covering the static subsite,
parseRoutes and mkYesod
- The Magic of Yesod, part 1 December 23, 2010 Explains the basics of Template Haskell and Quasi-Quotation, and
how it ties in with Hamlet, Cassius and Julius
- Custom Forms and the Form Monad October 20, 2010 Explains a new way of creating forms introduced in Yesod
0.6.
- Ajax Push July 22, 2010 An extension of the basic chat example to use Ajax push.
- The Handler Monad July 15, 2010 Slightly out-of-date, as we've migrated back to a form of either monad
transformer.
- Fringe benefits of type-safe URLs June 24, 2010 Covers breadcrumbs, dedicated static file servers and
authorization.
- RESTful Content June 18, 2010
Frequently Asked Questions
Perhaps it's not common to put a FAQ in a book, but it seems that there are a number of questions that come up regularly enough. Hopefully this will help. If you can think of any questions you would like added, please write a comment to this page.
Why does the scaffolded site use cassiusFileDebug and juliusFileDebug, but does not use hamletFileDebug?
For efficiency and type safety, the template lanugages used in Yesod are compile-time interpreted. This is great for production, but can slow down development. As a workaround, this languages provide a "debug" version of their run functions which does some initial compile-time parsing to determine which variables to use, and then reads the template at runtime to pick up any changes. The scaffolded site specifically avoids hamletFileDebug for two reasons
- It does not handle Hamlet's polymorphism. A Hamlet template can be converted to a type of Html, Hamlet or
Widget, while hamletFileDebug only works with Hamlet. In theory, this could be fixed.
- hamletFileDebug does not work at all when you add new variables to the template. This is a problem that cannot
be fixed. The first issue does not affect Cassius and Julius since they do not have any polymorphism. The second one is not as big a deal for Cassius and Julius: it's simply not as common to interpolate variables in your CSS and Javascript. I personally found that hamletFileDebug only worked for me half the time. Instead, a more robust solution is to use wai-handler-devel (also included in the scaffolded site). This will automatically reinterpret your app after a change to your Haskell or Hamlet template files. This is usually very fast for small sites, but gets slow for large sites. Hopefully in the near future, wai-handler-devel will switch from using the hint package to the plugins package, which will help alleviate that burden.
84 | OpenTopic | Appendices
Migration Guide: 0.6 to 0.7
The following is a migration guide from Yesod 0.6.* to Yesod 0.7.0. These are notes I took while performing the migration on the Haskellers.com website. It may not be completely exhaustive, but it should be a good start. If you run into issues which are not addressed here, please add a comment on this paragraph so that I can update the article and others can see what's missing.
No Warnings
Before you get started, I recommend you get your current code to compile without any warnings. That way, as you begin migrating, you'll know that any new warnings are caused by the migration process itself. In fact, you may even want to turn on -Wall -Werror in your cabal file.
Install yesod
In theory, this will simply require running cabal update && cabal install yesod. In practice, cabal may not be able to figure everything out for you. If you run into DLL hell, the simplest solution currently is to just delete your .ghc folder. Please note that you need to have the happy and alex packages installed to install yesod.
Update cabal file
Almost all underlying packages for Yesod have been upgraded, including WAI, Hamlet, etc. Make sure to include newer versions. For the most part, if you depend on the yesod package itself, you can leave off version bounds on underlying packages. However, cabal may not always do the right thing. Some specific notes:
- Obviously, make sure your version bounds on the yesod package allow 0.7.0. I recommend >= 0.7 && < 0.8
- wai-extra no longer provides SimpleServer. Instead, you should use Warp.
- Make sure to add a -threaded GHC option when compiling all executables, even devel-server.
Make sure to run cabal install to automatically install any dependencies. If you run into DLL hell here, then (surprise) an easy solution is to wipe out .ghc and run cabal install again.
Update your Hamlet templates
Hamlet 0.7 introduces major syntax changes to all three template languages (Hamlet, Cassius and Julius). It also includes a utility that will automatically convert your files to the new syntax. The following commands will overwrite your current files, so make sure you've backed up/committed first: hamlet6to7 *.hs hamlet6to7 Handler/*.hs hamlet6to7 hamlet/*.hamlet hamlet6to7 cassius/*.cassius hamlet6to7 julius/*.julius Unfortunately the tool gives a lot of false positives about mismatches in Hamlet. My best advice is to look at the diffs: this will simultaneously give you good confidence that the conversion worked correctly and get you familiar with the new syntax.
cabal install
Even though your code won't build yet, this will be a good way to see if you've set up your cabal file correctly. One common error you'll encounter is that many of the modules that used to be in the yesod package are now included in separate packages, such as Yesod.Helpers.Static now living in yesod-static. Make sure to add in any missing packages to your cabal file. Some likely offenders will be:
- warp
- yesod-form
- yesod-newsfeed
OpenTopic | Appendices | 85
- yesod-static
- Possibly need to move some dependencies (like ToHtml typeclass) from hamlet to blaze-html
- Control.Monad.Invert is no longer used; instead, use relevant modules from the monad-peel package (most likely
Control.Exception.Peel).
Show, Read and Eq for Html
Hamlet is now using blaze-html's Html datatype. While I think this is a good move overall, one downside is that blaze-html does not provide Show, Read and Eq instances of Html. When you include an Html value in a Persistent model, it won't be able to create those instances by default. If you get error messages, just add a single "deriving" line at the bottom. For example: Person name String bio Html becomes Person name String bio Html deriving
MonadInvertIO to MonadPeelIO
Nuff said :)
urlRenderOverride
The previous scaffolded version of urlRenderOverride no longer works. Use this one instead: urlRenderOverride a (StaticR s) = Just $ uncurry (joinPath a Settings.staticroot) $ renderRoute s urlRenderOverride _ _ = Nothing
runDB: need liftIOHandler
For the impatient: stick liftIOHandler at the beginning of runDB like so: runDB db = liftIOHandler $ fmap connPool getYesod >>= Settings.runConnectionPool db For everyone else: WAI 0.2 contained the request body as an Enumerator in the Request datatype, and an Application was Request -> IO Response. In WAI 0.3, an Application is Request -> Iteratee ByteString IO Response. Long story short: all code in your Handler function now lives on top of an Iteratee monad so that it can access the request body. This works great most of the time. The only downside is when you need to deal with exceptions. It's impossible to define a MonadPeelIO instance for Iteratee. Therefore, in Yesod 0.7, we have a new datatype called GGHandler, which is just a generalization of GHandler to allow an arbitrary inner monad. GHandler defaults to having an Iteratee on the inside. But when you need a MonadPeelIO (like we do in Persistent), you need to have an IO on the inside. Eventually though, you'll need to convert your GGHandler IO to a
- GHandler. That's what liftIOHandler does.
mime-mail Part constructor
mime-mail introduced a new record to the Part constructor- called partHeaders- that did not exist before. To get the same behavior as before, just pass in an empty list. This is especially relevant for users of yesod-auth's email plugin.
86 | OpenTopic | Appendices
Feed, formerly known as AtomFeed
Patrick Brisbin has become a co-maintainer on the yesod-newsfeed package, and added a feature I'm sure many people will appreciate: RSS feed support. The best part is that it uses the same datatype as the Atom feeds, so with just a few changes you can have both versions of newsfeed:
- Change AtomFeed to Feed
- Change AtomFeedEntry to FeedEntry
- Change records like atomTitle to feedTitle
- Add two new records: feedDescription and feedLanguage
- Change RepAtom to RepAtomRss
- Change atomFeed to newsFeed
- Change imported module to Yesod.Helpers.Feed
fileLookupDir no more
The standard line for getting static file serving previously was s = fileLookupDir Settings.staticdir
- typeByExt. In Yesod 0.7, we're migrating to the much more powerful yesod-app-static package. The replacement
for the above line is s = static Settings.staticdir.
MultiParamTypeClasses
A major new feature is the simplification of the dispatch code. While this almost entirely is transparent to the user,
- ne user-visible changes is the unification of YesodSite and YesodSubSite into YesodDispatch. As a result, your
Controller.hs file will now need MultiParamTypeClasses turned on.
String to ByteString
In a few places (redirectString, mime-type, sendFile) I've replaced Strings with ByteStrings. I'm not 100% certain this was the right choice, but it is more correct in the sense that these values are sent verbatim across the wire. A simple Data.ByteString.Char8.pack will get you the old behavior.
devel-server.hs
The code for devel-server.hs is now much simpler. Assuming your foundation datatype is MyApp, just use: import Yesod (develServer) main :: IO () main = develServer 3000 "Controller" "withMyApp"
lift, not liftHandler
Thanks to ##### ###### for pointing this one out: the liftHandler function has been removed. Instead, you can now use the standard Control.Monad.Trans.Class.lift function. Additionally, since the newIdent function has moved from the Widget to Handler monads, any calls you have now to newIdent will need to be lifted.
Migration Guide: 0.7 to 0.8
Following on the success of the previous migration guide, this chapter will similarly document the necessary changes to upgrade the Haskellers site to Yesod 0.8. As before, if you run into issues not covered here, please add it to the comments.
OpenTopic | Appendices | 87
No Warnings
Before you get started, I recommend you get your current code to compile without any warnings. That way, as you begin migrating, you'll know that any new warnings are caused by the migration process itself. In fact, you may even want to turn on -Wall -Werror in your cabal file.
Install yesod
In theory, this will simply require running cabal update && cabal install yesod. In practice, cabal may not be able to figure everything out for you. If you run into DLL hell, the simplest solution currently is to just delete your .ghc folder. Please note that you need to have the happy and alex packages installed to install yesod.
Update cabal file
Almost all underlying packages for Yesod have been upgraded, including WAI, Hamlet, etc. Make sure to include newer versions. For the most part, if you depend on the yesod package itself, you can leave off version bounds on underlying packages. However, cabal may not always do the right thing. Some specific notes:
- Obviously, make sure your version bounds on the yesod package allow 0.8.0. I recommend >= 0.8 && < 0.9
- There was a brief period where putting language extensions in the cabal file was considered a Good Thing. We
have now reversed our decision on this.
- Haskellers for 0.7 was still using FastCGI for production deployment. I'm finally switching it over to Warp this
time around. The important thing is to make sure the cpp-options: -DPRODUCTION is applied to the Warp- based executable as necessary.
- If you reference monad-peel, please replace it with monad-control.
- You likely need to add the new persistent-template package to the dependencies.
- Also, you'll likely need http-types.
cabal install
Even though your code won't build yet, this will be a good way to see if you've set up your cabal file correctly.
persistent-template
Some functions have moved around a bit. In particular, mkMigrate is no longer provided by Database.Persist.GenericSql, but by Database.Persist.TH. However, since the Yesod module now exports that entire module, you will likely be able to simply remove an import line.
Language Extensions
I'll assume that some people are simultaneously making the leap from GHC 6.12 to 7 (I recommend you do at this point, Warp performs much better on GHC 7). Here are some extension gotchas:
- You need to enable the TemplateHaskell language extension if you use any top-level TH splices. Otherwise
you'll see Parse error: naked expression at top level
- The syntax for quasi-quotation changed in GHC 7 to no longer need the dollar sign. While you can leave it in,
the compiler will give a warning. I'm keeping the dollar sign for the moment in all packages on Hackage to allow people to stick with GHC 6.12 for a bit longer, but in private packages, I recommend using GHC 7 syntax.
External File
This one isn't strictly necessary, but the scaffolded site is now using external files for model entity and routing
- declarations. This will allow us to develop standalone tools to make modifications to scaffolded sites. You may want
to consider doing the same thing. For example, if you have: mkPersist [$persist| Person
88 | OpenTopic | Appendices name String age Int |] You can put the entity itself in a file like entities and replace the quasi-quotation with $(persistFile "entities"). Similarly, for routing, just use the parseRoutesFile function.
toHtml
Hamlet is built on top of blaze-html. In the past, in order to convert a String to Html (and escape entities), you would use the string function. That function, and all its relatives (like text) have been deprecated in favor of toHtml.
MonadPeelIO to MonadControlIO
Nuff said :)
String to Text
This is the big one. We've moved virtually every usage of String to strict Text. Just follow the compiler warnings
- n this one: it's a bit of a pain to migrate this, but well worth it in potential performance improvements. The
OverloadedStrings extensions can help you big here. It's tempting to just start packing and unpacking all over the place. I tried this as well. But the fact is, this defeats all the possible performance benefits of the switch to Text, and is frankly harder to manage. It's much easier to just bite the bullet, switch your datatypes from String to Text, and then clean up once.
Persist Keys
In previous versions of Persistent, all database keys were stored as integers. However, the MongoDB backend is going to require a different datatype (it uses 12-byte strings), so we needed to make some changes. This means that Persistent keys are no longer instances of Integral. For the scaffolded site from previous version of Yesod, this has an effect on the implementation of the showAuthId, readAuthId, showAuthEmailId and readAuthEmailId functions. These functions have now been removed, and are instead implemented internally using the SinglePiece instance for keys. In general, if you need to serialize a key to/from text, you should use the SinglePiece instance. Most code doesn't need to do this too often, since Yesod handles it for you via type-safe URLs. But if the need arises, use toSinglePiece to produce a Text and fromSinglePiece to parse a Text.
Explicit Type Signatures
You may occassionally need to add explicit type signatures to deal with GHC 7's new type inferencer. This has affected me mostly with polymorphic Hamlet.
OpenTopic | Examples | 89
Examples
Example: Blog
<p>Well, just about every web framework I've seen starts with a blog tutorial- so here's mine! Actually, you'll see that this is actually a much less featureful blog than most, but gives a good introduction to Yesod basics. I recommend you start by <a href="/book/basics">reading the basics chapter</ a>.</p> <p>This file is literate Haskell, so we'll start off with our language pragmas and import statements. Basically every Yesod application will start off like this:</p> > {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} > import Yesod Next, we'll define the blog entry information. Usually, we would want to store the data in a database and allow users to modify them, but we'll simplify for the moment. > data Entry = Entry > { entryTitle :: String > , entrySlug :: String -- ^ used in the URL > , entryContent :: String > } Since normally you'll need to perform an IO action to load up your entries from a database, we'll define the loadEntries function to be in the IO monad. > loadEntries :: IO [Entry] > loadEntries = return > [ Entry "Entry 1" "entry-1" "My first entry" > , Entry "Entry 2" "entry-2" "My second entry" > , Entry "Entry 3" "entry-3" "My third entry" > ] Each Yesod application needs to define the site argument. You can use this for storing anything that should be loaded before running your application. For example, you might store a database connection there. In our case, we'll store our list of entries. > data Blog = Blog { blogEntries :: [Entry] } > type Handler = GHandler Blog Blog Now we use the first "magical" Yesod set of functions: mkYesod and
- parseRoutes. If you want to see *exactly* what they do, look at their Haddock
- docs. For now, we'll try to keep this tutorial simple:
> mkYesod "Blog" [parseRoutes| > / HomeR GET > /entry/#String EntryR GET > |] Usually, the next thing you want to do after a call to mkYesod is to create an instance of Yesod. Every Yesod app needs this; it is a centralized place to define some settings. All settings but approot have sensible defaults. In
90 | OpenTopic | Examples general, you should put in a valid, fully-qualified URL for your approot, but you can sometimes get away with just doing this: > instance Yesod Blog where approot _ = "" This only works if you application is being served from the root of your webserver, and if you never use features like sitemaps and atom feeds that need absolute URLs. We defined two resource patterns for our blog: the homepage, and the page for each entry. For each of these, we are allowing only the GET request method. For the homepage, we want to simply redirect to the most recent entry, so we'll use: > getHomeR :: Handler () > getHomeR = do > Blog entries <- getYesod > let newest = last entries > redirect RedirectTemporary $ EntryR $ entrySlug newest We go ahead and send a 302 redirect request to the entry resource. Notice how we at no point need to construct a String to redirect to; this is the beauty
- f type-safe URLs.
Next we'll define a template for entry pages. Normally, I tend to just define them within the handler function, but it's easier to follow if they're separate. Also for clarity, I'll define a datatype for the template
- arguments. It would also be possible to simply use the Entry datatype with
some filter functions, but I'll save that for a later tutorial. > data TemplateArgs = TemplateArgs > { templateTitle :: Html > , templateContent :: Html > , templateNavbar :: [Nav] > } The Nav datatype will contain navigation information (ie, the URL and title)
- f each entry.
> data Nav = Nav > { navUrl :: Route Blog > , navTitle :: Html > } And now the template itself: > entryTemplate :: TemplateArgs -> Hamlet (Route Blog) > entryTemplate args = [hamlet| > !!! > > <html> > <head> > <title>#{templateTitle args} > <body> > <h1>Yesod Sample Blog > <h2>#{templateTitle args} > <ul id="nav"> > $forall nav <- templateNavbar args > <li> > <a href="@{navUrl nav}">#{navTitle nav} > <div id="content"> > \#{templateContent args} > |]
OpenTopic | Examples | 91 Hopefully, that is fairly easy to follow; if not, please review the Hamlet
- documentation. Just remember that dollar signs mean Html variables, and at
signs mean URLs. Finally, the entry route handler: > getEntryR :: String -> Handler RepHtml > getEntryR slug = do > Blog entries <- getYesod > case filter (\e -> entrySlug e == slug) entries of > [] -> notFound > (entry:_) -> do > let nav = reverse $ map toNav entries > let tempArgs = TemplateArgs > { templateTitle = toHtml $ entryTitle entry > , templateContent = toHtml $ entryContent entry > , templateNavbar = nav > } > hamletToRepHtml $ entryTemplate tempArgs > where > toNav :: Entry -> Nav > toNav e = Nav > { navUrl = EntryR $ entrySlug e > , navTitle = toHtml $ entryTitle e > } All that's left now is the main function. Yesod is built on top of WAI, so you can use any WAI handler you wish. For the tutorials, we'll use the basicHandler that comes built-in with Yesod: it serves content via CGI if the appropriate environment variables are available, otherwise with simpleserver. > main :: IO () > main = do > entries <- loadEntries > warpDebug 3000 $ Blog entries
Example: Ajax
<p>We're going to write a very simple AJAX application. It will be a simple site with a few pages and a navbar; when you have Javascript, clicking on the links will load the pages via AJAX. Otherwise, it will use static HTML.</p> <p>We're going to use jQuery for the Javascript, though anything would work just fine. Also, the AJAX responses will be served as JSON. Let's get started.</p> > {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} > import Yesod > import Yesod.Helpers.Static > import Data.Monoid (mempty) Like the blog example, we'll define some data first. > data Page = Page > { pageName :: String > , pageSlug :: String > , pageContent :: String > } > loadPages :: IO [Page] > loadPages = return
92 | OpenTopic | Examples > [ Page "Page 1" "page-1" "My first page" > , Page "Page 2" "page-2" "My second page" > , Page "Page 3" "page-3" "My third page" > ] > data Ajax = Ajax > { ajaxPages :: [Page] > , ajaxStatic :: Static > } > type Handler = GHandler Ajax Ajax Next we'll generate a function for each file in our static folder. This way, we get a compiler warning when trying to using a file which does not exist. > staticFiles "static/yesod/ajax" Now the routes; we'll have a homepage, a pattern for the pages, and use a static subsite for the Javascript and CSS files. > mkYesod "Ajax" [$parseRoutes| > / HomeR GET > /page/#String PageR GET > /static StaticR Static ajaxStatic > |] <p>That third line there is the syntax for a subsite: Static is the datatype for the subsite argument; siteStatic returns the site itself (parse, render and dispatch functions); and ajaxStatic gets the subsite argument from the master argument.</p> <p>Now, we'll define the Yesod instance. We'll still use a dummy approot value, but we're also going to define a default layout.</p> > instance Yesod Ajax where > approot _ = "" > defaultLayout widget = do > Ajax pages _ <- getYesod > content <- widgetToPageContent widget > hamletToRepHtml [$hamlet| > \<!DOCTYPE html> > > <html> > <head> > <title>#{pageTitle content} > <link rel="stylesheet" href="@{StaticR style_css}"> > <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/ jquery.min.js"> > <script src="@{StaticR script_js}"> > \^{pageHead content} > <body> > <ul id="navbar"> > $forall page <- pages > <li> > <a href="@{PageR (pageSlug page)}">#{pageName page} > <div id="content"> > \^{pageBody content} > |] <p>The Hamlet template refers to style_css and style_js; these were generated by the call to staticFiles above. There's nothing Yesod-specific about the <a href="/static/yesod/ajax/style.css">style.css</a> and <a href="/static/ yesod/ajax/script.js">script.js</a> files, so I won't describe them here.</p>
OpenTopic | Examples | 93 <p>Now we need our handler functions. We'll have the homepage simply redirect to the first page, so:</p> > getHomeR :: Handler () > getHomeR = do > Ajax pages _ <- getYesod > let first = head pages > redirect RedirectTemporary $ PageR $ pageSlug first And now the cool part: a handler that returns either HTML or JSON data, depending on the request headers. > getPageR :: String -> Handler RepHtmlJson > getPageR slug = do > Ajax pages _ <- getYesod > case filter (\e -> pageSlug e == slug) pages of > [] -> notFound > page:_ -> defaultLayoutJson (do > setTitle $ string $ pageName page > addHamlet $ html page > ) (json page) > where > html page = [$hamlet| > <h1>#{pageName page} > <article>#{pageContent page} > |] > json page = jsonMap > [ ("name", jsonScalar $ pageName page) > , ("content", jsonScalar $ pageContent page) > ] <p>We first try and find the appropriate Page, returning a 404 if it's not
- there. We then use the applyLayoutJson function, which is really the heart
- f this example. It allows you an easy way to create responses that will be
either HTML or JSON, and which use the default layout in the HTML responses. It takes four arguments: 1) the title of the HTML page, 2) some value, 3) a function from that value to a Hamlet value, and 4) a function from that value to a Json value.</p> <p>Under the scenes, the Json monad is really just using the Hamlet monad, so it gets all of the benefits thereof, namely interleaved IO and enumerator
- utput. It is pretty straight-forward to generate JSON output by using the
three functions jsonMap, jsonList and jsonMap. One thing to note: the input to jsonScalar must be HtmlContent; this helps avoid cross-site scripting attacks, by ensuring that any HTML entities will be escaped.</p> <p>And now our typical main function. We need two parameters to build our Ajax value: the pages, and the static loader. We'll load up from a local directory.</p> > main :: IO () > main = do > pages <- loadPages > let s = static "static/yesod/ajax" > warpDebug 3000 $ Ajax pages s
Example: Form
<p>Forms can be a tedious part of web development since they require synchronization of code in many different areas: the HTML form declaration, parsing of the form and reconstructing a datatype from the raw values. The
94 | OpenTopic | Examples Yesod form library simplifies things greatly. We'll start off with a basic application.</p> > {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell #-} > import Yesod > import Control.Applicative > import Data.Text (Text) > data FormExample = FormExample > type Handler = GHandler FormExample FormExample > mkYesod "FormExample" [$parseRoutes| > / RootR GET > |] > instance Yesod FormExample where approot _ = "" Next, we'll declare a Person datatype with a name and age. After that, we'll create a formlet. A formlet is a declarative approach to forms. It takes a Maybe value and constructs either a blank form, a form based on the original value, or a form based on the values submitted by the user. It also attempts to construct a datatype, failing on validation errors. > data Person = Person { name :: Text, age :: Int } > deriving Show > personFormlet p = fieldsToTable $ Person > <$> stringField "Name" (fmap name p) > <*> intField "Age" (fmap age p) We use an applicative approach and stay mostly declarative. The "fmap name p" bit is just a way to get the name from within a value of type "Maybe Person". > getRootR :: Handler RepHtml > getRootR = do > (res, wform, enctype) <- runFormGet $ personFormlet Nothing <p>We use runFormGet to bind to GET (query-string) parameters; we could also use runFormPost. The "Nothing" is the initial value of the form. You could also supply a "Just Person" value if you like. There is a three-tuple returned, containing the parsed value, the HTML form as a widget and the encoding type for the form.</p> <p>We use a widget for the form since it allows embedding CSS and Javascript code in forms directly. This allows unobtrusive adding of rich Javascript controls like date pickers.</p> > defaultLayout $ do > setTitle "Form Example" > form <- extractBody wform <p>extractBody returns the HTML of a widget and "passes" all of the other declarations (the CSS, Javascript, etc) up to the parent widget. The rest of this is just standard Hamlet code and our main function.</p> > addHamlet [$hamlet| > <p>Last result: #{show res} > <form enctype="#{enctype}"> > <table> > \^{form} > <tr> > <td colspan="2"> > <input type="submit"> > |] >
OpenTopic | Examples | 95 > main = warpDebug 3000 FormExample
Example: Widgets
> {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell #-} > import Yesod > import Yesod.Helpers.Static > import Yesod.Form.Jquery > import Yesod.Form.Nic > import Control.Applicative > import Data.Text (unpack) > > data HW = HW { hwStatic :: Static } > type Handler = GHandler HW HW > mkYesod "HW" [$parseRoutes| > / RootR GET > /form FormR > /static StaticR Static hwStatic > /autocomplete AutoCompleteR GET > |] > instance Yesod HW where approot _ = "" > instance YesodJquery HW > instance YesodNic HW > wrapper h = [$hamlet| > <#wrapper>^{h} > <footer>Brought to you by Yesod Widgets™ > |] > getRootR = defaultLayout $ wrapper $ do > i <- lift newIdent > setTitle $ string "Hello Widgets" > addCassius [$cassius| > #$i$ > color: red|] > addStylesheet $ StaticR $ StaticRoute ["style.css"] [] > addStylesheetRemote "http://localhost:3000/static/style2.css" > addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/ jquery.min.js" > addScript $ StaticR $ StaticRoute ["script.js"] [] > addHamlet [$hamlet| > <h1 ##{i}>Welcome to my first widget!!! > <p > <a href=@RootR@>Recursive link. > <p > <a href=@FormR@>Check out the form. > <p .noscript>Your script did not load. :( > |] > addHtmlHead [$hamlet|<meta keywords=haskell|] > > handleFormR = do > (res, form, enctype, nonce) <- runFormPost $ fieldsToTable $ (,,,,,,,,) > <$> stringField "My Field" Nothing > <*> stringField "Another field" (Just "some default text") > <*> intField "A number field" (Just 5) > <*> jqueryDayField def "A day field" Nothing > <*> timeField "A time field" Nothing > <*> boolField "A checkbox" (Just False) > <*> jqueryAutocompleteField AutoCompleteR "Autocomplete" Nothing > <*> nicHtmlField "HTML" > (Just $ string "You can put <rich text> here") > <*> maybeEmailField "An e-mail addres" Nothing > let mhtml = case res of
96 | OpenTopic | Examples > FormSuccess (_, _, _, _, _, _, _, x, _) -> Just x > _ -> Nothing > defaultLayout $ do > addCassius [$cassius| > .tooltip > color: #666 > font-style: italic > textarea.html > width: 300px > height: 150px|] > addWidget [$hamlet| > <form method="post" enctype="#{enctype}"> > <table> > \^{form} > <tr> > <td colspan="2"> > \#{nonce} > <input type="submit"> > $maybe html <- mhtml > \#{html} > |] > setTitle $ string "Form" > > main = warpDebug 3000 $ HW $ static "static" > > getAutoCompleteR :: Handler RepJson > getAutoCompleteR = do > term <- runFormGet' $ stringInput "term" > jsonToRepJson $ jsonList > [ jsonScalar $ unpack term ++ "foo" > , jsonScalar $ unpack term ++ "bar" > , jsonScalar $ unpack term ++ "baz" > ]
Example: Generalized Hamlet
This example shows how generalized hamlet templates allow the creation of different types of values. The key component here is the HamletValue typeclass. Yesod has instances for: * Html * Hamlet url (= (url -> [(String, String)] -> String) -> Html) * GWidget s m () This example uses all three. You are of course free in your own code to make your own instances. > {-# LANGUAGE QuasiQuotes, TypeFamilies, MultiParamTypeClasses, OverloadedStrings, TemplateHaskell #-} > import Yesod > data NewHamlet = NewHamlet > mkYesod "NewHamlet" [$parseRoutes|/ RootR GET|] > instance Yesod NewHamlet where approot _ = "" > type Widget = GWidget NewHamlet NewHamlet > > myHtml :: Html > myHtml = [$hamlet|<p>Just don't use any URLs in here!|] > > myInnerWidget :: Widget ()
OpenTopic | Examples | 97 > myInnerWidget = do > addHamlet [$hamlet| > <div #inner>Inner widget > #{myHtml} > |] > addCassius [$cassius| >#inner > color: red|] > > myPlainTemplate :: Hamlet NewHamletRoute > myPlainTemplate = [$hamlet| > <p > <a href=@{RootR}>Link to home > |] > > myWidget :: Widget () > myWidget = [$hamlet| > <h1>Embed another widget > \^{myInnerWidget} > <h1>Embed a Hamlet > \^{addHamlet myPlainTemplate} > |] > > getRootR :: GHandler NewHamlet NewHamlet RepHtml > getRootR = defaultLayout myWidget > > main :: IO () > main = warpDebug 3000 NewHamlet
Example: Pretty YAML
<p>This example uses the <a href="http://hackage.haskell.org/package/data-
- bject-yaml">data-object-yaml package</a> to display YAML files as cleaned-up
- HTML. If you've read through the other tutorials, this one should be easy to
follow.</p> > {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} > import Yesod > import Data.Object > import Data.Object.Yaml > import qualified Data.ByteString as B > import qualified Data.ByteString.Lazy as L > data PY = PY > type Handler = GHandler PY PY > mkYesod "PY" [$parseRoutes| > / Homepage GET POST > |] > instance Yesod PY where approot _ = "" > template :: Maybe (Hamlet url) -> Hamlet url > template myaml = [$hamlet| > !!! > > <html> > <head> > <meta charset="utf-8"> > <title>Pretty YAML
98 | OpenTopic | Examples > <body> > <form method="post" action="" enctype="multipart/form-data" .> > \File name: > <input type="file" name="yaml"> > <input type="submit"> > $maybe yaml <- myaml > <div>^{yaml} > |] > getHomepage :: Handler RepHtml > getHomepage = hamletToRepHtml $ template Nothing > postHomepage :: Handler RepHtml > postHomepage = do > (_, files) <- runRequestBody > fi <- case lookup "yaml" files of > Nothing -> invalidArgs ["yaml: Missing input"] > Just x -> return x > so <- liftIO $ decode $ B.concat $ L.toChunks $ fileContent fi > hamletToRepHtml $ template $ Just $ objToHamlet so > objToHamlet :: StringObject -> Hamlet url > objToHamlet (Scalar s) = [$hamlet|#{s}|] > objToHamlet (Sequence list) = [$hamlet| > <ul > $forall o <- list > <li>^{objToHamlet o} > |] > objToHamlet (Mapping pairs) = [$hamlet| > <dl > $forall pair <- pairs > <dt>#{fst pair} > <dd>^{objToHamlet $ snd pair} > |] > main :: IO () > main = warpDebug 3000 PY
Example: Internationalization
> {-# LANGUAGE QuasiQuotes #-} > {-# LANGUAGE TemplateHaskell #-} > {-# LANGUAGE TypeFamilies #-} > {-# LANGUAGE MultiParamTypeClasses #-} > {-# LANGUAGE OverloadedStrings #-} > import Yesod > import Data.Monoid (mempty) > import Data.Text (Text) > data I18N = I18N > type Handler = GHandler I18N I18N > mkYesod "I18N" [$parseRoutes| > / HomepageR GET > /set/#Text SetLangR GET > |] > instance Yesod I18N where > approot _ = "http://localhost:3000" > getHomepageR :: Handler RepHtml
OpenTopic | Examples | 99 > getHomepageR = do > ls <- languages > let hello = chooseHello ls > let choices = > [ ("en", "English") :: (Text, Text) > , ("es", "Spanish") > , ("he", "Hebrew") > ] > defaultLayout $ do > setTitle "I18N Homepage" > addHamlet [$hamlet| > <h1>#{hello} > <p>In other languages: > <ul> > $forall choice <- choices > <li> > <a href="@{SetLangR (fst choice)}">#{snd choice} > |] > chooseHello :: [Text] -> Text > chooseHello [] = "Hello" > chooseHello ("he":_) = "Shalom" > chooseHello ("es":_) = "Hola" > chooseHello (_:rest) = chooseHello rest > getSetLangR :: Text -> Handler () > getSetLangR lang = do > setLanguage lang > redirect RedirectTemporary HomepageR > main :: IO () > main = warpDebug 3000 I18N