Yesod Tutorial 2. Playing with Routes and Links

17 Oct 2012 Bartosz Milewski

In the previous tutorial I described a single-file Yesod program that ran a very simple web site. I will continue adding new features to this program to illustrate various aspects of Yesod while at the same time teaching you elements of Haskell.

I'll be using the style of programming that's quite appropriate for a tutorial, but you should realize that an industrial web site requires much more structure. For instance, most of the HTML, CSS, and JavaScript code would be put in separate files rather than embedded directly in Haskell code. The Yesod framework supports this professional style of programming by providing a ready-made scaffolding for a web site, which you can fill with your custom code. We'll talk about scaffolding in the later installments of this tutorial. For now let's explore some of the customization points I talked about in the previous tutorial: adding routes, adding links between pages, and exploring the whamlet EDSL. For your convenience, here's our starting point from the previous tutorial:

{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
             TemplateHaskell, OverloadedStrings #-}
import Yesod

data Piggies = Piggies

instance Yesod Piggies

mkYesod "Piggies" [parseRoutes|
  / HomeR GET
|]

getHomeR = defaultLayout [whamlet|
  Welcome to the Pigsty!
|]

main = warpEnv Piggies

Routes

First thing that comes to mind is to enrich our web site by adding more pages and some kind of navigation between them. We'll create the "about" page by first adding a new route to our list of routes:

mkYesod "Piggies" [parseRoutes|
  /      HomeR  GET
  {-hi-}/about AboutR GET</span>{-/hi-}
|]

Remember, routes are defined using a simple EDSL parseRoutes. You add a route by adding a line to the quasi-quoted argument to the (Template Haskell) function mkYesod. The first element in the line is the route itself -- /about in this case. If the URL of the web site is, say, http://fpcomlete.com, the URL of the about page will be http://fpcomlete.com/about.

The second element in the line is the resource name, AboutR, which we'll be using internally to address this page. Finally, GET is the HTTP request method that will be used to access this page. (Note: This is just a simple example of a static route. Later you'll see how to construct dynamic routes parameterized by integers, dates, names, and so on.)

An interesting exercise is to try to compile our program with just this one change. The error message you get is very telling:

Not in scope: `getAboutR'

Each route passed to mkYesod has to have a corresponding handler defined, and we are missing one for the About page. Let's stub it for now:

getAboutR = defaultLayout [whamlet||]

(Later I'll show you how to use undefined to stub function bodies -- we could have done it here but we would need a type signature for getAboutR and we haven't gotten that far yet.)

We already had a handler for the home route, but let's play with it a little:

getHomeR = defaultLayout [whamlet|
  Welcome to the Pigsty!
  <br>
  <a href=@{AboutR}>About Us.
|]

You can see now that whamlet is really an enhanced version of HTML. You can insert HTML tags in it -- like the line break <br> or the anchor <a ...>. I used a few features of whamlet that make it more convenient than straight HTML (which, by the way, you can also use in Yesod -- more about it later).

One of the features is that, just like in Haskell, whitespace is significant and the compiler pays attention to formatting. It means that if you open a tag on a separate line, you don't need to provide the closing tag. I used this feature to elide the closing </a> tag after "About Us." Granted, this is a minor convenience, but it removes a major source of annoyance: when you forget the closing tag or mismatch the opening and closing tags. Meaningful formatting goes even further: You may use indentation to nest your tags. For instance, you can create a paragraph block, complete with the embedded link:

getHomeR = defaultLayout [whamlet|
    Welcome to the Pigsty!
    {-hi-}<p>{-/hi-}
        If you'd like to know more
        <a href=@{AboutR}>About Us
        we'll be happy to oblige.
|]

This is what this page looks like in the browser:

About Page

The indentation trick works because of the nesting property of HTML tags. (It also works in formatting Haskell code because of the nesting property of scopes.)

Type-Safe URLs

There's more interesting stuff going on here -- interpolation. That's when you insert in-place code in the middle of formatting, as in:

<a href=@{AboutR}>About Us.
In regular HTML you would use a URL (whether absolute or relative) in a link tag. For instance:
<a href="/about">About Us.
That works, but it introduces a fragile dependency. Essentially, once you decide on the URLs for your internal pages, you should never change them, because you're going to break internal links. Yes, there are utilities that can scan your web site and report broken links, but that's a crutch. What you really want is the web site with broken links not to compile. And that's what Yesod provides through type-safe URLs.

So how does this work? You've already seen one part of the solution: a level of indirection between routes and resources. The route for the about page, which is also its URL, is defined as /about. The resource corresponding to it is a Haskell data constructor, called AboutR (we already have one such data constructor, HomeR; AboutR is the second data constructor for the same type -- see "A Bit of Haskell" at the end).

In Yesod you define your routes once and then use their resource names everywhere. How is this different? Since each resource is not a string but a separate Haskell type constructor, if you change its name in any place you will get an undefined symbol error and the code will not compile.

Let's try it. If I make a typo (can you spot it?):

<a href=@{AbuotR}>About Us.
I get the following error:
Not in scope: data constructor `AbuotR'
(The GHC compiler is careful not to say AbuotR is undefined, because it could be defined somewhere else. Instead it tells you that AbuotR's definition is not in scope. If you come from C++, you need to get used to "not in scope" meaning "undefined".)

Type-safe URLs also help you detect errors in more complex cases, for instance when you're dealing with parameterized routes. In that case the type constructor for a resource takes an argument (an integer, a date, or a user name). If you call it with the wrong type of argument, the program will not compile.

You've probably figured it out by now, or remember from the previous tutorial, but let me repeat: The syntax for embedding routes inside whamlet is to enclose them with @{ and }. We'll see other types of interpolation soon.

To finish this example, let's expand the stub handler for the new route. We already know the name of the handler from the error message we got in the beginning, but let me remind you that the name is constructed from a lowercase request type, get, followed by the resource name, AboutR:

getAboutR = defaultLayout [whamlet|
  Enough about us!
  <br>
  <a href={-hi-}@{HomeR}{-/hi-}>Back Home.
|]

This code is very similar to the root handler, except that it contains a link back to the root page instead. You should be getting the hang of it by now. Try adding more pages to our web site, cross-linking them, and playing with HTML tags (various levels of headings, lists, spans, etc.).

Conclusion

The important lesson is that Yesod's creative use of Haskell's type system makes web applications more robust. We've just seen the example of type-safe URLs, and we'll see more such examples in the future.

A Bit of Haskell

In this installment we talked about data constructors like HomeR or AboutR so it makes sense to learn a bit about defining data types in Haskell. We've already seen one example in the previous tutorial, that of the Yesod foundation type for our website:

data Piggies = Piggies

A bit of syntax: There are three ways of defining types in Haskell:

  • Using data you can define an arbitrary (algebraic) data type. This is the workhorse of the type system.
  • Using type you can define an alias or a synonym for a type. This is equivalent to C++ typedef. For instance, a String is an alias for a list of characters:
    type String = [Char]
    All functions that work on lists also work with Strings.
  • Using newtype you can create a new type from an existing one. For instance, if you have a function that takes, say, a first name and a last name, and you don't want the two to be confused, you create new types for them, complete with data constructors:
    newtype FirstName = FirstName String
    newtype LastName  = LastName String
    Now, if you pass a String or a LastName where FirstName is expected, you'll get an error. You could accomplish the same with a data definition, but newtype is quicker and more efficient.
For now, let's concentrate on data.

The Piggies definition is interpreted as follows: We are defining a type Piggies on the left hand side of the equal sign. The Piggies on the right hand side is a data constructor, which can be used to create values of type Piggies. These two names don't have to be the same. In fact, you can try changing the definition to:

data Piggies = MakePiggies
and see which parts of code require the type name and which require the type constructor.

The line:

instance Yesod Piggies
doesn't change. It states that the type Piggies is an instance of the type class Yesod (more about type classes in the future).

The (Template Haskell) function mkYesod also requires the name of the type (as string):

mkYesod "Piggies" [parseRoutes|
  /      HomeR  GET
  /about AboutR GET
|]
But the argument to warpDebug must be a value, so we need a data constructor to create it:
main = warpDebug 3000 MakePiggies
We've learned in this tutorial that resource names are data constructors. So what is the data type that they construct? The long answer would require the knowledge of Haskell type families (that's what the language pragma TypeFamilies at the top of the file is for), so I'll postpone it till much later. For now you can just imagine that there is a data type called RoutePiggies and its definition is:
data RoutePiggies = HomeR | AboutR
Now we have two data constructors on the right hand side separated by a vertical bar (pronounced "or"). We use these data constructors to create values that are passed to whamlet through escape sequences @{...}. If you try to pass anything else, like a string or a widget, you'll get a really scary compiler error.

Another example of this type of data definition is the Bool type:

data Bool = True | False
You read this definition as "Bool is either True of False." You are free to think of data types with constructors that take no arguments (and that's what we've seen so far) as corresponding to enumerations in other languages. We'll talk more about defining data types with non-trivial constructors and about recursive and parameterized data types in the future.

Important: The names of types and data constructors must start with a capital letter. This rule applies to buit-in types as well. Function and variable names always start with a lower case letter.

Some Basic Data Types

  • Char: Unicode character. A list of Char is [Char] (more about lists later).
  • Bool: An enumeration of True and False.
  • Int: Native integer. There are also fixed-width numeric types defined in modules Data.Int (signed) and Data.Word (unsigned).
  • Integer: More expensive infinite precision integer. It lets you calculate things like factorials of large numbers. It is implemented using a highly optimized GMP library, so it's not as bad as it looks.
  • Double: Native floating-point number.

Acknowledgments

I'd like to thank Michael Snoyman and Andres Löh for reviewing my posts. I'm also grateful to many people who commented on my previous tutorials and discussed them on reddit.

Appendix

For your reference, here's the complete program:

{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
             TemplateHaskell, OverloadedStrings #-}
import Yesod

data Piggies = Piggies

instance Yesod Piggies

mkYesod "Piggies" [parseRoutes|
  /      HomeR  GET
  /about AboutR GET
|]

getHomeR = defaultLayout [whamlet|
  Welcome to the Pigsty!
  <br>
  <a href=@{AboutR}>About Us.
|]

getAboutR = defaultLayout [whamlet|
  Enough about us!
  <br>
  <a href=@{HomeR}>Back Home.
|]

main = warpEnv Piggies
comments powered by Disqus