Haskell - Basics - Developing Web Apps with Haskell and Yesod, Second Edition (2015)

Developing Web Apps with Haskell and Yesod, Second Edition (2015)

Part I. Basics

Chapter 2. 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! by Miran Lipovača (No Starch Press)

§ Real World Haskell by Bryan O’Sullivan, John Goerzen, and Donald Bruce Stewart (O’Reilly)

Additionally, there are a number of great articles on School of Haskell.

In order to use Yesod, you’re going to have to know at least the basics of Haskell. Additionally, Yesod uses some features of Haskell that aren’t covered in most introductory texts. While this book assumes the reader has a basic familiarity with Haskell, this chapter is intended to fill in the gaps.

If you are already fluent in Haskell, feel free to completely skip this chapter. Also, if you would prefer to start off by getting your feet wet with Yesod, you can always come back to this chapter later as a reference.

Terminology

Even for those familiar with Haskell as a language, there can occasionally be some confusion about terminology. Let’s establish some base terms that we can use throughout this book:

Data type

This is one of the core building blocks for a strongly typed language like Haskell. Some data types (e.g., Int) can be treated as primitive values, while other data types will build on top of these to create more complicated values. For example, you might represent a person with:

dataPerson=PersonTextInt

Here, the Text would give the person’s name, and the Int would give the person’s age. Due to its simplicity, this specific example type will recur throughout the book.

There are essentially three ways you can create a new data type:

§ A type declaration such as type GearCount = Int. This merely creates a synonym for an existing type. The type system will do nothing to prevent you from using an Int where you asked for a GearCount. Using this can make your code more self-documenting.

§ A newtype declaration such as newtype Make = Make Text. In this case, you cannot accidentally use a Text in place of a Make; the compiler will stop you. The newtype wrapper always disappears during compilation and will introduce no overhead.

§ A data declaration such as Person. You can also create algebraic data types (ADTs)—for example, data Vehicle = Bicycle GearCount | Car Make Model.

Data constructor

In our examples, Person, Make, Bicycle, and Car are all data constructors.

Type constructor

In our examples, Person, Make, and Vehicle are all type constructors.

Type variables

Consider the data type data Maybe a = Just a | Nothing. In this case, a is a type variable.

NOTE

In both our Person and Make data types, our data type and data constructor share the same name. This is a common practice when dealing with a data type with a single data constructor. However, it is not a requirement; you can always name the data types and data constructors differently.

Tools

There are two main tools you’ll need for Haskell development. The Glasgow Haskell Compiler (GHC) is the standard Haskell compiler, and the only one officially supported by Yesod. You’ll also need Cabal, which is the standard Haskell build tool. Not only do we use Cabal for building our local code, but it can automatically download and install dependencies from Hackage, the Haskell package repository.

The Yesod website keeps an up-to-date quick start guide that includes information on how to install and configure the various tools. It’s highly recommended that you follow these instructions. In particular, these steps make use of Stackage to avoid many common dependency-resolution issues.

If you decide to install your tools yourself, make sure to avoid these common pitfalls:

§ Some JavaScript tools that ship with Yesod require the build tools alex and happy to be installed. These can be added with cabal install alex happy.

§ Cabal installs an executable to a user-specific directory, which needs to be added to your PATH. The exact location is OS-specific; be sure to add the correct directory.

§ On Windows, it’s difficult to install the network package from source, as it requires a POSIX shell. Installing the Haskell Platform avoids this issue.

§ On Mac OS X, there are multiple C preprocessors available: one from Clang, and one from GCC. Many Haskell libraries depend on the GCC preprocessor. Again, the Haskell Platform sets things up correctly.

§ Some Linux distributions—Ubuntu in particular—typically have outdated packages for GHC and the Haskell Platform. These may no longer be supported by the current version of Yesod. Check the quick start guide for minimum version requirements.

§ Make sure you have all necessary system libraries installed. This is usually handled automatically by the Haskell Platform, but may require extra work on Linux distros. If you get error messages about missing libraries, you usually just need to apt-get install or yum install the relevant libraries.

Once you have your toolchain set up correctly, you’ll need to install a number of Haskell libraries. For the vast majority of the book, the following command will install all the libraries you need:

cabal update && cabal install yesod yesod-bin persistent-sqlite yesod-static

Again, refer to the quick start guide for the most up-to-date and accurate information.

Language Pragmas

GHC will run by default in something very close to Haskell98 mode. It also ships with a large number of language extensions, allowing more powerful typeclasses, syntax changes, and more. There are multiple ways to tell GHC to turn on these extensions. For most of the code snippets in this book, you’ll see language pragmas, which look like this:

{-# LANGUAGE MyLanguageExtension #-}

These should always appear at the top of your source file. Additionally, there are two other common approaches:

§ On the GHC command line, pass an extra argument: -XMyLanguageExtension.

§ In your cabal file, add a default-extensions block.

I personally never use the GHC command-line argument approach. It’s a personal preference, but I like to have my settings clearly stated in a file. In general, it’s recommended to avoid putting extensions in your cabal file; however, this rule mostly applies when writing publicly available libraries. When you’re writing an application that you and your team will be working on, having all of your language extensions defined in a single location makes a lot of sense. The Yesod scaffolded site specifically uses this approach to avoid the boilerplate of specifying the same language pragmas in every source file.

We’ll end up using quite a few language extensions in this book (at the time of writing, the scaffolding uses 13). We will not cover the meaning of all of them. Instead, see the GHC documentation.

Overloaded Strings

What’s the type of "hello"? Traditionally, it’s String, which is defined as type String = [Char]. Unfortunately, there are a few limitations with this:

§ It’s a very inefficient implementation of textual data. We need to allocate extra memory for each cons cell, plus the characters themselves each take up a full machine word.

§ Sometimes we have string-like data that’s not actually text, such as ByteStrings and HTML.

To work around these limitations, GHC has a language extension called OverloadedStrings. When enabled, literal strings no longer have the monomorphic type String; instead, they have the type IsString a -> a, where IsString is defined as:

classIsString a where

fromString ::String->a

There are IsString instances available for a number of types in Haskell, such as Text (a much more efficient packed String type), ByteString, and Html. Virtually every example in this book will assume that this language extension is turned on.

Unfortunately, there is one drawback to this extension: it can sometimes confuse GHC’s type checker. For example, imagine we use the following code:

{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}

importData.Text (Text)

classDoSomething a where

something ::a ->IO ()

instanceDoSomethingStringwhere

something _=putStrLn "String"

instanceDoSomethingTextwhere

something _=putStrLn "Text"

myFunc ::IO ()

myFunc =something "hello"

Will the program print out String or Text? It’s not clear. So instead, you’ll need to give an explicit type annotation to specify whether "hello" should be treated as a String or Text.

NOTE

In some cases, you can overcome these problems by using the ExtendedDefaultRules language extension, though we’ll instead try to be explicit in the book and not rely on defaulting.

Type Families

The basic idea of a type family is to state some association between two different types. Suppose we want to write a function that will safely take the first element of a list. But we don’t want it to work just on lists; we’d like it to treat a ByteString like a list of Word8s. To do so, we need to introduce some associated type to specify what the contents of a certain type are:

{-# LANGUAGE TypeFamilies, OverloadedStrings #-}

importData.Word (Word8)

importqualifiedData.ByteStringas S

importData.ByteString.Char8 () -- get an orphan IsString instance

classSafeHead a where

typeContent a

safeHead ::a ->Maybe (Content a)

instanceSafeHead [a] where

typeContent [a] =a

safeHead []=Nothing

safeHead (x:_) =Just x

instanceSafeHeadS.ByteStringwhere

typeContentS.ByteString=Word8

safeHead bs

| S.null bs =Nothing

| otherwise =Just $ S.head bs

main ::IO ()

main =do

print $ safeHead ("" ::String)

print $ safeHead ("hello" ::String)

print $ safeHead ("" ::S.ByteString)

print $ safeHead ("hello" ::S.ByteString)

The new syntax is the ability to place a type inside of a class and instance. We can also use data instead, which will create a new data type instead of referencing an existing one.

NOTE

There are other ways to use associated types outside the context of a typeclass. For more information on type families, see the Haskell wiki page.

Template Haskell

Template Haskell (TH) is an approach to code generation. We use it in Yesod in a number of places to reduce boilerplate, and to ensure that the generated code is correct. Template Haskell is essentially Haskell that generates a Haskell abstract syntax tree (AST).

NOTE

There’s actually more power in TH than that, as it can in fact introspect code. We don’t use these facilities in Yesod, however.

Writing TH code can be tricky, and unfortunately there isn’t very much type safety involved. You can easily write TH that will generate code that won’t compile. This is only an issue for the developers of Yesod, not for its users. During development, we use a large collection of unit tests to ensure that the generated code is correct. As a user, all you need to do is call these already existing functions. For example, to include an externally defined Hamlet template (discussed in Chapter 4), you can write:

$(hamletFile "myfile.hamlet")

The dollar sign immediately followed by parentheses tell GHC that what follows is a Template Haskell function. The code inside is then run by the compiler and generates a Haskell AST, which is then compiled. And yes, it’s even possible to go meta with this.

A nice trick is that TH code is allowed to perform arbitrary IO actions, and therefore we can place some input in external files and have it parsed at compile time. One example usage is to have compile-time–checked HTML, CSS, and JavaScript templates.

If our Template Haskell code is being used to generate declarations and is being placed at the top level of our file, we can leave off the dollar sign and parentheses. In other words:

{-# LANGUAGE TemplateHaskell #-}

-- Normal function declaration, nothing special

myFunction =...

-- Include some TH code

$(myThCode)

-- Or equivalently

myThCode

It can be useful to see what code is being generated by Template Haskell for you. To do so, you should use the -ddump-splices GHC option.

NOTE

There are many other features of Template Haskell not covered here. For more information, see the Haskell wiki page.

Template Haskell introduces something called the stage restriction, which essentially means that code before a Template Haskell splice cannot refer to code in the Template Haskell, or what follows. This will sometimes require you to rearrange your code a bit. The same restriction applies to QuasiQuotes.

Out of the box, Yesod is really geared for using code generation to avoid boilerplate, but it’s perfectly acceptable to use Yesod in a Template Haskell–free way. There’s more information on that in Chapter 20.

QuasiQuotes

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed arbitrary content within our Haskell source files. For example, we mentioned previously the hamletFile TH function, which reads the template contents from an external file. We also have a quasiquoter namedhamlet that takes the content inline:

{-# LANGUAGE QuasiQuotes #-}

[hamlet|<p>This is quasi-quoted Hamlet.|]

The syntax is set off using square brackets and pipes. The name of the quasiquoter is given between the opening bracket and the first pipe, and the content is given between the pipes.

Throughout the book, we will frequently use the QQ approach over a TH-powered external file, as the former is simpler to copy and paste. However, in production, external files are recommended for all but the shortest of inputs, as it gives a nice separation of the non-Haskell syntax from your Haskell code.

API Documentation

The standard API documentation program in Haskell is called Haddock. The standard Haddock search tool is called Hoogle. I recommend using FP Complete’s Hoogle search and its accompanying Haddocks for searching and browsing documentation, because the database covers a very large number of open source Haskell packages, and the documentation provided is always fully generated and known to link to other working Haddocks.

The more commonly used sources for these are Hackage itself, and Haskell.org’s Hoogle instance. The downsides to these are that—based on build issues on the server—documentation is sometimes not generated, and the Hoogle search defaults to searching only a subset of available packages. Most importantly for us, Yesod is indexed by FP Complete’s Hoogle, but not by Haskell.org’s.

If you run into types or functions that you do not understand, try doing a Hoogle search with FP Complete’s Hoogle to get more information.

Summary

You don’t need to be an expert in Haskell to use Yesod—a basic familiarity will suffice. This chapter hopefully gave you just enough extra information to feel more comfortable as you follow along throughout the rest of the book.