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 executablesdependencies. 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.

Conclusion