
Haskell Quickstart Field Guide
Short Guide on Getting Started with Haskell
September 22, 2021
Haskell has been an interest of mine for quite some time. I am far from an expert in the language, as it is not my day to day language. Every time I return to experiment with Haskell, I feel that I have to fumble around a bit.
The Haskell ecosystem has matured quite a bit over the years since I’ve learned the language. Things have matured and it has become easier to return to and run older projects. Even so, there are rough edges and pain points that I have experienced as a returning Haskell enthusiast. This post is a reference guide where I attempt to point explain the tools I currently use, list a few quickfixes, and link to common documentation newcomers and enthusiast should probably actively have open.
Stack
The primary tool that I currently use for experimenting with Haskell is Stack. Stack gives you the ability to isolate your project’s compiler and dependencies. With this alone, you can have multiple projects of varying age using different version of the Haskell compiler, and dependencies. Stack makes it a no brainer to start using Haskell.
I will not go into details about installing Stack, but you can find instructions on: haskellstack.org.
Once Stack is installed on your machine, there are a few essential commands to effectively work.
Starting a New Project
Starting a new project to work on is as simple as running stack new <PROJECT-NAME>
.
This will generate the basic project structure:
<PROJECT-NAME>/
app/
Main.hs
src/
Lib.hs
test/
Spec.hs
stack.yaml
package.yaml
Note that some files were omitted for brevity.
The entry point to your Haskell Program will then be app/Main.hs
. The file
may contain your logic or it can be split out into different modules inside
of src/
.
Building a Project
To get the build started for your project you can run stack build
. This command
will run compile and link your program. Any compile errors as a result of invalid
syntax or unresolved dependencies will be displayed in the command’s output.
A usful flag for this command is --file-watch
. When using this flag, the build
will start a long running process in the foreground that continually compiles and
links your program’s files after any change. This is one of the best ways to get
continous feedback about your program. Type mismatch errors and errors around
unresolved dependencies are all displayed on screen.
stack build --file-watch
While building a project is great, it’s not enough to compile the project, it’s important that we are able to execute our compiled program.
Running a Project
Stack provides two mechanism for running our project. Stack’s run
and exec
commands are similar in that they will both run the compiled executable for our
project. However, they differ in that stack run
will build the project, then
execute the compiled program. stack exec <project-name>-exec
will run the a
previously compiled artifact.
For simplicity, stack run
is good enough in most circumstances.
Running Tests
While we have not talked about tests, Stack does generate a directory for
tests along with some of the necessary configuration. Tests are then runnable
via stack test
. This command runs the main file, test/Spec.hs
and logs
all output to the screen. The basic template does not actually test anything,
nor does it load any unit testing framework, but the bones for testing are
there. Additionally, state test
supports the --file-watch
options, so that
we can rerun tests whenever one of our files changes. This is extremely useful
because it goes beyond just asserting that out program compiles, but we
potentially have a way to assert that the behavior of our program is correct!
Prelude, LTS, & Libraries
We’ve established that we’re using Stack, and we’ve got our basic project compiling and running. However, now it’s time to start writing the logic for our program. This is where we need to start sorting through all of the documentation out there, and finding the pertinent bits to accomplish our task.
Your task at hand may be widlly different from mine, so I hope to give you some quick links to keep around as you navigate to just the right permutation of haskell syntax or function calls.
Stack LTS
Since we’re using Stack, we need to keep in mind that we are potentially limited to libary versions that are part of our “resolver”. A resolver is an identifier for all sets of packages that are known to work well together, as defined by their target dependency version numbers or ranges.
It’s important to note that not all packages are part of all resolvers. At times you may be limited to older resolvers because one or more packages have been bumped in version, and have become potentially incompatible with older consumers.
The libraries and version part of a resolver can be found on the resolver’s page
on www.stackage.org. For example the LTS-18.7
resolver’s set
of packages can be seen here.
Prelude
Prelude is the basic set of function that are available for you anywhere
at anytime. These function will be of paramount utility, and will likely
recieve heavy use. You can find the version of base
being used by your
project by looking the resolver’s package list.
Have a look at your pakage.yaml
to see which version or version range
is in use by your project. This will typically be a range. However, a
specific version will typically be defined by the resolver version.
dependencies:
- base >= 4.7 && < 5
This means we’ll be using the latest 4.x
that’s greater than 4.7
. At
the time of writing that would be 4.14.3.0
as part of resolver LTS-18.7
.
base-4.14.3.0
This document has a lot of good bits about basic data type, list operations, and handling input and output. Make sure to grock this document and keep a bookmark to it while writing your program.
Even though Prelude will give us alot of functions for free, there are many cases where we are in need to dependencies. Next let’s have a look at how we can add dependencies to our project.
Adding Dependencies
Dependencies are going to be essential part of many projects. With Stack, we need to keep a few things in mind when looking to add dependencies. First, we need to find out if the version of the dependency we want is part of the resolver by looking at the resolver page. If the package is not part of the resolver, we can try and find another resolver that’s got the package we need, or we can try to force Stack to use the a specific version.
Resolver Available Dependencies
The best case is when the dependency is already part of the resolver. In that
case all we need to do is update our package.yaml
with the name of our package.
If our modules in our src
directory are making use of the dependency, we can
update the library
section’s dependencies
. If we are using the dependency in our
modules part of our app
directory, then we need to update executables
’ dependencies
.
And likewise, if our test
directory is making use of a dependency we’d update our
tests
section dependencies
.
For example if each module needed text
dependency to make use of Data.Text
.
library:
source-dirs: src
dependencies:
- text
executables:
<project-name>-exe:
main: Main.hs
source-dirs: app
dependencies:
- text
tests:
main: Main.hs
source-dirs: test
dependencies:
- text
Resolver Unavailable Dependencies
If a resolver does not contain a dependency, we can force Stack to
use a specific dependency by updating extras-deps
inside of stack.yaml
.
The dependency can be a fixed version that’s published on hackage, or a git respository with a reference to a commit hash.
For example, if we really wanted to pin our project to an older verison of
the text
package, we could do so via:
extra-deps:
- text-1.0.0.0
While this works, this might not be the best for our project. Stack would possibly try to resolve other version of dependencies which could conflict with our project.
To learn more about adding dependencies have a look at Stack’s documentation around adding dependencies.