<- ahri.net

Practical Haskell programs from scratch - a quick and easy guide

Replacing Bash scripts with cross-platform Haskell

Last updated: 2018-11-11

TL;DR

  1. Install Stack
  2. Add a header to your scripts and maybe chmod 755 them
    #!/usr/bin/env stack
    {- stack --resolver lts-12.9 script -}
    
  3. REPL: $ stack ghci --resolver lts-12.9 --package PKG_NAME foo.hs
  4. Watch: $ ghcid -c 'stack ghci --resolver lts-12.9 --package PKG_NAME foo.hs'
  5. Compile: $ stack ghc --resolver lts-12.9 --package PKG_NAME foo.hs
  6. Search: $ hoogle '[a] -> Int' or online
  7. Format: $ brittany --write-mode inplace foo.hs
  8. Lint: $ hlint --refactor --refactor-options="-is" foo.hs
  9. Tags: $ fast-tags foo.hs
  10. A script to do most of this for you

A brief intro

Haskell is a practical language, one in which we can be fully aware of the effects our programs might have; we are able to write safer programs that look nicer than any mainstream language, ones that are performant and that interact efficiently with the outside world.

That’s not to say there are no difficulties in getting started; as with every language there’s an ecosystem to discover, a package manager to get to grips with, libraries to choose between, editor plugins to hunt down, and plenty of other things to distract you from actually writing some code.

Here, I will supply what I consider to be the fastest way to get up and running with Haskell - it’s opinionated, and I’m sure you’ll want to tweak and improve on it as you learn more and form your own opinions and tastes, but it’s a straightforward start. I will keep it updated with what I consider to be the state of the art.

One of the early problems newcomers to the community encounter is how to lay out a project; Where do the files go? How do I declare my dependencies? Stack? Cabal? Nix? To combat this paralysis without forcing you down a specific route I’m opting to help you write self-contained Haskell scripts. We’ll use Stack as a package manager for compilers and libraries alike.

Install basic requirements

So let’s get going! Note that we’ll be doing a lot in the terminal (or a Command/PowerShell window - though only the former comes with examples) and that the commands you need to run are signified by a dollar sign followed by the command to copy/paste - you don’t need to copy the dollar sign too! First we need Stack, follow the Stack installation instructions. If you already have Stack installed, make sure it’s up-to-date via $ stack upgrade

Great, now in a terminal let’s check the version - if the following command doesn’t work, try restarting your terminal program $ stack --version.

Verify that your version of Stack is now at 1.7.1 or above; if not you may need to fix your PATH in accordance with the location of the executable following the stack upgrade command. On Mac/Linux this likely means adding $HOME/.local/bin to the start of your PATH, and similarly on Windows by adding %USERPROFILE%\AppData\Roaming\local\bin to the start of your System (not User) PATH, then restarting your terminal (and maybe your computer).

Write a program

Now, let’s write a Haskell program. Fire up an editor of your choice (yep; Notepad will do fine) and paste in the following, noting that using tabs is a bad idea in Haskell - always indent with spaces, then save it somewhere you’ll be able to access from the terminal as “first_haskell_program.hs”:

#!/usr/bin/env stack
{- stack --resolver lts-12.9 script -}

import System.Environment
import System.Exit
import System.IO

main :: IO ()
main = do
    args <- getArgs
    let putStrLnErr = hPutStrLn stderr
    if length args > 0
        then putStrLnErr (show args)
        else die "No args passed in"

    putStrLn "stdin:"
    getContents >>= putStrLn

Ok, before we run it, a quick warning - Stack is going to realise that we’re missing a Haskell compiler so it’s going to download it, which could take a little while as it’s around 200MB. Let’s try it out:

$ stack first_haskell_program.hs arg1 arg2 arg3

Type some stuff in when prompted with “stdin:”, and press CTRL+D (or CTRL+C in Windows) when you’re done.

So let’s see where we’re up to: we installed all the tools we need to execute a Haskell program, wrote one to interact with the outside world (you, in this case) and ran it. That wasn’t so hard was it?

An aside: on a posix system we can make first_haskell_program.hs executable with chmod 755 first_haskell_program.hs and then run it directly: ./first_haskell_program.hs.

Now, there’s a little bit more to feeling comfortable with a language than just running a program, so let’s dig a little deeper; we’re going to import a third-party library, get acquainted with the REPL, sort out auto-checking of your program, compile your program to a native binary, and to cap it all off we’re going to gain the ability to search for functions we can’t name and locate ones we can.

Third-party dependencies

Languages tend to come alive when you can see how much the community contributes in terms of useful, stable libraries. One thing I’m confident you don’t want to do is to implement your own Regular Expression library (at least not yet!) so let’s leverage Hackage via Stack to grab a regex library and implement a simple grep.hs:

#!/usr/bin/env stack
{- stack --resolver lts-12.9 script
    --package regex-posix
-}

import Control.Monad
import Data.Foldable
import System.Environment
import System.Exit
import System.IO
import Text.Regex.Posix

main :: IO ()
main = do
    args <- getArgs
    when (null args) $ die "Provide a pattern"

    let pattern = head args

    input <- getContents
    matches <- sequence [putStrLn line | line <- lines input, line =~ pattern]

    if length matches /= 0
        then exitSuccess
        else exitFailure

On a non-Windows system:

$ echo nope | stack grep.hs ^y; echo Exit: $?
Exit: 1
$ echo yup | stack grep.hs ^y; echo Exit: $?
yup
Exit: 0

On Windows (cmd):

> echo nope | stack grep.hs ^y & echo Exit: %errorlevel%
Exit: 1
> echo yep | stack grep.hs ^y & echo Exit: %errorlevel%
yep
Exit: 0

We’re testing the program here by sending in a single line using echo that’s being checked against the pattern ^y which is a regex requiring that the input start with a “y” - you can check this behaviour against egrep (since we’re using regex-posix; use regex-compat for something more similar to grep) if you have it available on your system.

A “REPL”; read-evaluate-print-loop

This is our playground - we can try things out, ask the compiler what’s going on, and poke bits of our program.

We can run it, specifying the same resolver and packages as we use in the header of our script file.

$ stack ghci --resolver lts-12.9 --package regex-posix grep.hs

Now we can interrogate the compiler to tell us about our program:

> :t main
main :: IO ()
> :i main
main :: IO ()
        -- Defined at /some/path/grep.hs:13:1

Ok, that’s not that thrilling, wait, maybe we’re not familiar with the =~ operator - where did that come from?

> :i (=~)
(=~) ::
  (RegexMaker Regex CompOption ExecOption source,
   RegexContext Regex source1 target) =>
  source1 -> source -> target
        -- Defined in ‘Text.Regex.Posix.Wrap’

Ah, so it came from Text.Regex, makes sense :)

How about some other functions?

> :t length
length :: Foldable t => t a -> Int
> :t lines
lines :: String -> [String]

And how does that all fit together?

> :t length (lines "")
length (lines "") :: Int

Sometimes it’s not so easy to find an easy-to-type bit of data like "" so we can just pretend:

> :t length (lines (undefined::String))
length (lines (undefined::String)) :: Int

Watch

It’s nice to know that things still compile while we’re editing code, so wouldn’t it be helpful if a tool sat there checking our work for us without us having to jump out and run our script all the time?

$ stack install ghcid

My process is always:

  1. Find a command that works to get me a GHCI session
  2. Use that exact command with ghcid

So let’s use the above command with ghcid - fire up a new terminal window and run this:
$ ghcid -c "stack ghci --resolver lts-12.9 --package regex-posix grep.hs"

That will sit there checking your code compiles and giving you error output - try breaking your program; when you save ghcid will re-check it. Nice.

Compilation

You probably noticed that running stack grep.hs is a bit slow as it compiles the program before running each time. To avoid this we can compile to native code:

$ stack ghc --resolver lts-12.9 --package regex-posix grep.hs
[1 of 1] Compiling Main             ( grep.hs, grep.o )
Linking grep ...

Then, on a non-Windows system:

$ echo nope | ./grep ^y; echo Exit: $?
Exit: 1
$ echo yep | ./grep ^y; echo Exit: $?
yep
Exit: 0

and on a Windows system (via cmd):

> echo nope | grep.exe ^y & echo Exit: %errorlevel%
Exit: 0
>echo yep | grep.exe ^y & echo Exit: %errorlevel%
yep
Exit: 1

Searching functions

We have a great resource available to us in the ability to search all the packages published to Hackage via Hoogle, and we can do this offline by installing the command-line util:

$ stack install hoogle
$ hoogle generate

Now we can search for functions by signature or name, and hoogle will do a fuzzy lookup for us:

Format

There are many tools to format Haskell code, and all sorts of configuration options. As this is a guide to getting set up quickly I’ve chosen one: $ stack install brittany

To format your existing code:
$ brittany --write-mode inplace foo.hs

Lint

Linting is a useful way of learning nicer ways to accomplish things with your code - it takes an existing code file and suggests improvements.
$ stack install hlint apply-refact

To run hlint interactively:
$ hlint --refactor --refactor-options="-is" foo.hs

Tags

This is perhaps not so useful if you’re not using vim or emacs as an editor; it generates a database of “tags” (names used in your code) which allows you to jump to functions in any file in a project - in our case there’s just one, but it can still be useful if your script is large.

I opted for fast-tags because it’s useful that it has its own parser, so it can still generate tags even if your Haskell script is not compiling right now:

$ stack install fast-tags

Generate tags with:

$ fast-tags -R for a whole directory tree or
$ fast-tags foo.hs for a single file

Formulation

Of course I use a script to help me out with this stuff as I’m likely to forget these neat features. It’s a self-contained Haskell script that removes the need to specify LTS and package lists every time, and of course it works on multiple platforms! It also serves as a concrete example of a script converted from Bash to Haskell.

Another example is a script to pick random Steam games which I converted from Python to Haskell and is interesting as it accesses the Windows registry or the filesystem depending on which platform it’s running on thereby highlighting the practical functionality Haskell offers in these cases.

Other resources

Turtle is a very interesting library that eases the way from Bash to Haskell, making scripting really accessible, it also has a really nice tutorial making it very easy to get started with.

Shelly is more of a departure from Bash, but adds plenty of convenience to dealing with processes, files and directories.

Thanks

Contact

All comments, corrections and suggestions welcome via email or on reddit.