Hacking With Haskell

Hacking With Haskell
Photo by Fotis Fotopoulos / Unsplash

When learning a new programming language we are rarely short on guides on the language itself. But so much of the experience of a language is tied up with the ability to exercise it. When you're at the foothills of a learning curve, building an effective experimentation platform is tricky, but oh so valuable.

Here's what I discovered works well when hacking with Haskell. The options are bewildering, but this working set is simple!

tl;dr:

  1. Use ghci as your primary experimentation interface, but only for one-liners, transient work or to exercise other sources (see The REPL).
    a. Don't expect it to parse arbitrary Haskell - for that see step 2.
    b. Add :set +m to ~/.ghci (see Multi-line blocks)
    c. Use let before multi-line blocks (see Multi-line blocks)
  2. Put anything else, and anything you want to re-use, in a .hs file (see Import source).
    a. Name it, say, scratch.hs and invoke ghci from the same directory.
    b. Use :l scratch to import your file in ghci.
    c. Use :r every time you make a change to your file.
  3. Use VS Code and the Haskell and Haskell Syntax Highlighting extensions to edit your .hs files (see The Editor).
    a. Harness the power of linting that pure languages afford!
  4. Create executables by putting your entry statement after main = in a .hs file (see The Execution).
    a. Compile with ghc my_file_containing_main.hs additional_file.hs.
The REPL
The Editor

On with the details...

The REPL

Interpreted languages get a huge on-boarding benefit because they lend themselves to a read–eval–print loop - known as a REPL. In a REPL you don't immediately get the benefit of compilation (ie. primarily execution speed and syntax/reference checking), but when you need to experiment, those things can wait. I posit that Python's rise to fame is in a big way due to the fact that the primary interface is a REPL: type Python, get results.

Many modern languages recognise that compilation is an irreplaceable feature, but REPL's are mighty handy too. Haskell is one of those - it's a compiled language, but these days it comes bundled with GHCi, which is the interactive version of the compiler ghc. It is, by far, the best way to get acquainted with the language and conduct experiments. The latter task, I suggest, is where we actually spend most of our programming effort.

Every REPL however, presents an imitation of the language. What can be typed into a REPL prompt is a subset of what the language can accommodate. Part of the challenge of learning Haskell then, is learning the translations between the Haskell language and what the REPL will parse.

Here are some key differences that will make working with the REPL effective.

Starting

If you've installed Haskell in the usual way, you can invoke the REPL anywhere by running ghci:

$ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Prelude>

The Prelude in the prompt indicates that by default, ghci has imported everything in the "Prelude", which is a default set of libraries considered core to Haskell.

Stopping

To get out of the REPL, use :quit.

Importing modules

In Haskell, definitions in one source file are made accessible to other source files by putting

Module moduleName where

at the top of the file. Optionally, exported definitions from the file can be limited by listing them in parentheses:

Module moduleName (export1, export2) where

On the consumption end, you can import modules using the import keyword and specifying the module name. Similarly, you can limit the imported definitions by listing them in parentheses after the module name, like

import moduleName (export1)

In the REPL, you can use the import keyword in the same way.

Prelude> import Data.List (break)
Prelude Data.List> 

The module name will be added to the prompt so you can always see what is in scope.

If the module is installed but in a "hidden" package, you will be prompted on how to enable it:

Prelude> import Control.Parallel.Strategies

<no location info>: error:
    Could not load module ‘Control.Parallel.Strategies’
    It is a member of the hidden package ‘parallel-3.2.2.0’.
    You can run ‘:set -package parallel’ to expose it.
    (Note: this unloads all the modules in the current scope.)

To avoid the warning, and unloading all other modules, you can invoke ghci with the -package flag. Eg:

$ ghci -package parallel

If the module cannot be found at all, but is part of one of the "safe" Haskell packages in Stackage, you can install it for use in ghci generally using stack install moduleName. Note this is a much simplified version of how you would use stack in its primary capacity as a build tool in a Haskell project. In that scenario, stack install should be used with caution.

Import source

When you're hacking, creating modules is a long way from view. Instead you want to be able to make a change to some source and then immediately poke it in the REPL. Fortunately, ghci has just the trick. The :load command (or :l for short) will import all the symbols from .hs file whether it is in a module or not.

Prelude> :l scratch
[1 of 1] Compiling Main             ( scratch.hs, interpreted )
Ok, one module loaded.
*Main> 

To locate the .hs file, either start ghci from the same directory as the file, or use :cd to navigate there.

Even better, if you update the source file the :reload (or :r for short) command will reload whatever file was previously loaded with :load.

*Main> :r
Ok, one module loaded.
*Main> 

Note that the prompt has changed to Main. The limitation of :load is that the symbols in the loaded file are imported into a common namespace called Main, and only one Main exists at a time. That's great for hacking, but it does mean if you want to :load more than one source file at a time, you're going to need to put all but one in a Module and use import instead. Even worse, all your local definitions typed into ghci will be overwritten! All the more reason to copy them into your scratch.hs file instead.

Multi-line blocks

One of the major limitations of a REPL is the difficulty of inputing constructs that span multiple lines. It can be done, but since hitting the enter key usually indicates that the current line should be executed, there are some caveats. There are three tricks to make life easier:

  1. ghci can be set to multiline mode with :set +m. The REPL will then do its best to detect when a line is incomplete and automatically wait for extra lines. But given the challenges of understanding indentation (and the fact that in ghci the tab key invokes command completion, not the whitespace character!) and line ending semantics at the best of times, you can do better.
  2. Multi-line blocks can be prefixed the line :{ and suffixed by the line :}. This avoids the mystery of auto-detection in +m, but it introduces a REPL-only construct. That introduces a barrier when learning the language and makes it harder to flip between the REPL and pure Haskell source because it changes the interpretation. For example, let ... do blocks must be modified to fit in :{ ... :} wrappings.
  3. The standard Haskell let keyword can be used on a line by itself. This unambiguously starts a multi-line block that terminates explicitly when an empty line is inputted. Crucially, the same multi-line block is valid Haskell, even though the line ending after the let is unnecessary. A combination of +m and starting with let is an excellent way to maintain parity between Haskell source and what you type into the REPL.

Enabling Language Extensions

Some of Haskell's language extensions (eg. TupleSections and TemplateHaskell) are very common. In Haskell they are enabled with:

{-# LANGUAGE TupleSections #-}

But in the REPL you must use:

:set -XTupleSections

or add the -XTupleSections flag when invoking ghci from the command line.

The Editor

Any text editor will do, but Visual Studio Code happens to have some excellent extensions that make life hacking Haskell good. Those extensions are:

As well as the standard code navigation, code completion, and compiler warnings and errors you might expect from a language server, the Haskell extension really shows off the language by also providing:

  • Suggestions from hlint, which demonstrate spectacular static analysis insights. Honestly this is one of my most significant sources of Haskell education.
  • Code evaluation (eg. in live comments).
  • Documentation from Hackage.
  • Code modding with retrie.
  • Code generation from holes using Wingman.

The Execution

The REPL is where all the action is, but at the end of the day Haskell needs to live beyond the command prompt. The transition is quite easy.

Simply take what you've plugged into the REPL, put it in a filename.hs file of your choice, and prefix it with main =. Put your import statements above that, add any other definitions, and you're ready to go. Compile with ghc, specifying the file with main and any other source files, and your executable is ready for execution.

$ cat helloworld.hs
import qualified Data.Text as T

main = putStrLn $ T.unpack msg

msg :: T.Text
msg = T.pack "Hello, world!"

$ ghc helloworld.hs
[1 of 1] Compiling Main             ( helloworld.hs, helloworld.o )
Linking helloworld ...
$ ./helloworld 
Hello, world!