Mini intro slide deck
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.
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
.cabal
files, 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 called cabal-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 ()
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:
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 Int
or newtype 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
Terms:
Fruit
is a type constructor
Fruit
is also a type
Apple
, Banana
, and Pear
are data constructors
Person
is both a type and data constructor
- They live in separate namespaces, that's fine
Type variables:
data Maybe a = Nothing | Just a
a
is a type variable
Maybe
is a type constructor
- However,
Maybe
is not a type!
Maybe
has kind Type -> Type
, aka * -> *
Maybe Int
is 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