Applied Haskell 101
This page is the beginning of the Applied Haskell course. For more information, see the syllabus. The goal here is to provide some basic information on:
- What "Applied Haskell" means
- The tools you'll be using
- How to get more help
What is Applied Haskell?
Applied Haskell is a course developed over the years by the FP Complete team. It focuses on bridging the gap between Haskell basics to the ability to write production-grade Haskell applications. In our experience, there are a few important pieces necessary to make this happen:
- Understand the tooling
- Knowledge of the basic libraries
- Understand how best to structure an application
- Understand how to get more help and information
This course will not teach you every detail of every library. But hopefully by the end you'll be able to find any library you need, read its documentation, and use it.
About Haskell
Haskell is in many ways a revolutionary language. It was innovative when it first came on the scene decades ago, and remains innovative today. Haskell continues to add cutting-edge features, especially at the type level, to an industrial-strength language.
We don't care about that here. In Applied Haskell, we're going to treat Haskell as a "getting things done" language. To borrow phrase, we're going to be "brutally pragmatic." Haskell delivers a huge amount of value with a subset of its features. We're going to focus on those features and the tools and libraries necessary to take advantage of them. We'll also be covering Haskell idioms and patterns so you can pick up common code more quickly, and discuss some runtime system idiosyncrasies.
You should know the basics of Haskell to follow along properly. You do not need to know advanced language theory, category theory, or any other surprising topics.
Tooling
This course will assume that you'll be using the Stack build tool. Working with other tooling like Cabal or Nix will work as well, but may require more fiddling. For following this course, it's recommended to use Stack to avoid wasting time.
Stack
Stack is a build tool for Haskell. It can also install your compiler toolchain (and does so by default). It focuses on reproducible build plans. As mentioned, there are other tools, but we'll be using Stack. It comes first in this list as it's the first one you should install, by following the get started instructions.
If you already have Stack on your machine, you can usually upgrade to
the latest version by running stack upgrade. Now that you have
Stack, we can move on to...
GHC
GHC is the de facto standard Haskell compiler. You can install a compiler version by running a command line:
$ stack setup ghc-8.4.4
Typically you won't have to do this explicitly. When Stack notices you're missing a GHC version, it will install it for you.
GHC can both compile code and run it interpreted, as well as provide
you with an interactive REPL. You can do those directly with,
respectively, the ghc, runghc, and ghci executables. Stack does
not place these executables on the user PATH, so if you'd like to run
them, you'll typically use stack ghc, stack runghc, and stack ghci.
If you'd like more information on interacting in these three ways, check out:
The lessons in this course mostly stick to using the script approach.
Cabal build system
Apologies in advance, the terminology is about to get a bit complicated.
Stack is built on top of the Cabal build system. When you install a
package with Stack (e.g., by running stack build conduit), you're
installing a Cabal package. Cabal refers to a few different things:
- A file format for
.cabalfiles, which provide metadata on a package - A design of a build system
- A library for performing these builds, called
Cabal - A command line executable. The executable is called
cabal, and the package it comes from is calledcabal-install.
When using Stack, you'll use the stack executable instead of the
cabal/cabal-install executable. Other than that, you're using the
same Cabal components. Both tools (and Nix, for that matter) can
install the same packages, so there's high overlap.
The one final complication is that there's another file format called
hpack. This file format is YAML based (with files called
package.yaml), and is used to generate .cabal files. Stack
supports these out of the box, and many Stack projects will provide
package.yaml files, and have auto-generated .cabal files.
More information on all of this at:
Note that these are more advanced issues which you probably won't run into immediately. It's good to know where more information is when you do hit a roadblock.
Stackage
There are thousands of Haskell packages available online on Hackage, the central open source package repository. In order to make it easier to find a working build plan, and provide consistent behavior for multiple users, the Stackage project provides curated snapshots of packages which are tested to work together. There are two different sets of Stackage snapshots:
- Long Term Support (LTS) Haskell comes out with snapshots about once per week. They are named lts-x.y (e.g., lts-12.21). Within a major version number, we mostly include non-breaking changes to packages. This makes it relatively easy to upgrade to more recent LTS minor versions.
- Stackage Nightly builds are daily and provide no compatibility guarantees. They are named nightly-YYYY-MM-DD (e.g., nightly-2019-03-10).
stackage.org provides a few resources. Some notable ones:
- You can perform a Hoogle search, which allows you to search an
entire snapshot for names and types. For example, you can search
for
map. - You can browse API documentation for a specific package, e.g. the docs for conduit.
Note that Hackage also provides API documentation. I recommend using Stackage's in general for two reasons:
- When you use Stackage's docs, you're getting docs for a specific Stackage snapshot. This means that any links to dependencies is the same dependency used in that snapshot.
- Sometimes the doc builder can have trouble building docs on Hackage, such as due to not finding a working build plan. With few exceptions, if a package is in a Stackage snapshot, it means that a working build plan was found.
hlint
The hlint tool is a great linter. It not only provides ways to
improve your code, but in the process can teach you about better ways
of writing code you weren't aware of. Install hlint with:
$ stack install --resolver lts hlint
To see an example of hlint's power, save the following to Main.hs
and then run hlint Main.hs:
main :: IO ()
main = do
mapM putStrLn ["Hello", "World"]
pure ()
Code formatting
Unfortunately in the Haskell world, code formatting isn't quite at the level of rigour as, say, Go. There are different coding styles that are commonly used, and (at least to my knowledge at time of writing) none of the tools can be guaranteed to perfectly format every valid Haskell program. Two commonly used tools are:
- stylish-haskell, which cleans up things like import statements
- hindent, which does a more traditional code formatting.
Editor integration/IDEs
There are lots of different pieces of integration for lots of editors, with various IDE support. This is a topic that's often in flux, depending on which tools have upgraded to the most recent release of GHC. For learning, I recommend: stick to simple syntax highlighting. If you find integrations that work well for you, great! The unfortunate reality is that what works for one person doesn't reliably work for others.
One tool which I will mention is ghcid. It is fully usable from the command line (in fact, that's its intended use case), and provides fast auto-rebuild support using the GHC interpreter. Basically:
- Run
stack install --resolver lts ghcid - Go into a directory with a Stack project
- Run
ghcid - Fix all the bugs that appear on your screen
- ...
- Profit!
Web resources
There are many resources available for Haskell across the web. In addition to some of the links above:
- The learn page on this site
- The community page on this site
- haskell.org
- The Yesod Web Framework
- School of Haskell
Nomenclature
Before diving into the rest of this course, a quick recap of some nomenclature to be familiar with:
Data types
- Type synonyms:
type String = [Char] - Newtypes:
newtype Age = Age Intornewtype Age = Age { unAge :: Int } - Data declaration
- Product type:
data Person = Person Name Age - Product w/record:
data Person = Person { personName :: Name, personAge :: Age } - Enum:
data Fruit = Apple | Banana | Pear - Mix them (proper sums):
data Age = UnknownAge | KnownAge Int
- Product type:
Terms:
Fruitis a type constructorFruitis also a typeApple,Banana, andPearare data constructorsPersonis both a type and data constructor- They live in separate namespaces, that's fine
Type variables:
data Maybe a = Nothing | Just aais a type variableMaybeis a type constructor- However,
Maybeis not a type! Maybehas kindType -> Type, aka* -> *Maybe Intis a type
More on data types in next section.
Partial/total functions
Bottom value: when evaluated, throws a runtime exception or loops infinitely
bottom1 = error "I'm bottom!"
bottom2 = undefined
bottom3 = _ -- probably fails at compile time
bottom4 = let x = x in x -- infinite loop, runtime may detect it
Total function produces a non-bottom output for all non-bottom input.
Partial function may produce a bottom output for some non-bottom input.
Partiality can sneak in:
- Partial pattern matches
- Lambdas on sum types
- Infinite loops
- Turn on
-Wall!
Language extensions
50% of all production Haskell code is language extension and import lines
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GADTs #-}
Added at the top of your file. Basic structure:
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
-- Above two lines for scripts, we'll cover that below
{-# LANGUAGE OverloadedStrings #-}
-- {-# LANGUAGE ... #-}
{-# OPTIONS_GHC -Wall #-} -- better in package.yaml file, below
module Main (main) where
import Control.Monad (when)
-- import ...
main :: IO ()
main = putStrLn "Finally, some actual code!"
See: https://github.com/commercialhaskell/rio#language-extensions
Syntactic sugar everywhere
Function calls:
foo x y = ...
foo = \x y -> ...
foo = \x -> \y -> ...
All the same, allow for partial function application (not the same as partial functions!).
Pattern matching:
fromMaybe def Nothing = def
fromMaybe _def (Just x) = x
fromMaybe def mx =
case mx of
Nothing -> def
Just x -> x
{-# LANGUAGE LambdaCase #-}
fromMaybe def = \case
Nothing -> def
Just x -> x
Also:
if x then y else z
case x of
True -> y
False -> z