Hacking With Haskell
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:
- 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. Uselet
before multi-line blocks (see Multi-line blocks) - Put anything else, and anything you want to re-use, in a
.hs
file (see Import source).
a. Name it, say,scratch.hs
and invokeghci
from the same directory.
b. Use:l scratch
to import your file inghci
.
c. Use:r
every time you make a change to your file. - 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! - Create executables by putting your entry statement after
main =
in a.hs
file (see The Execution).
a. Compile withghc my_file_containing_main.hs additional_file.hs
.
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 i
nteractive 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:
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 inghci
thetab
key invokes command completion, not the whitespace character!) and line ending semantics at the best of times, you can do better.- 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. - 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 thelet
is unnecessary. A combination of+m
and starting withlet
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:
- Haskell (connects to the Haskell Language Server to provide compiler insights)
- Haskell Syntax Highlighting (makes your eyelashes pop)
- Haskell GHCi Debug Adapter Phoityne (much more work to get going, but it's the way to go when you're ready to climb that mountain)
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!