Being a Haskell developer means going back and forth between
a programming editor and the
REPL
(GHCi). You go a long way, faster, when you try things in the REPL
to «get a feel», before making them part of your actual programs.
That’s why I always have a terminal window running ghci
, generally
started as stack repl
, because I prefer
Stack to
handle per-project dependencies.
I configure my preferred Stack Resolver for the «global project»
under ~/.stack/global-project/stack.yaml
resolver: ltrs-23.8
packages: []
extra-deps: []
and then install several Haskell tools into my local path.
I get up-to-date outstanding Haskell applications such as
pandoc
and Shake
,
and always use this global project for trying things
without having to start a new project.
Once installed, a «vanilla» REPL startup would look like this
$ stack repl
Note: No project package targets specified, so a plain ghci will be started with
no package hiding or package options.
You are using snapshot: lts-23.8
If you want to use package hiding and options, then you can try one of the
following:
* If you want to start a different project configuration than
/home/emhn/.stack/global-project/stack.yaml, then you can use stack init
to create a new stack.yaml for the packages in the current directory.
* If you want to use the project configuration at
* /home/emhn/.stack/global-project/stack.yaml,
* then you can add to its 'packages' field.
Configuring GHCi with the following packages:
GHCi, version 9.8.4: https://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /home/emhn/.cache/stack/ghci-script/2a3bbd58/ghci-script
ghci>
It’s possible to customize GHCi’s configuration by having a
file named .ghci
placed at the project’s root directory. That
is, you could have different customizations for different
projects. If you place the file at ~/.ghci
, it will become
your personal configuration both for the «global project»
and any other project, unless overriden by per project ones.
What do I need?
In my line of work and research interests, I often have to deal with obscure libraries, Unicode, deeply nested data structures, and «make it faster so the natives go crazy». This means I need GHCI to provide, at least:
Type-contextual automatic conversion for literal strings. This saves me having to type the explicit conversion from «poor man’s datatype» run-of-the-mill literal strings into the tagged type a library might need. A consequence of becoming all too familiar with the plethora of Haskell string types,
Selective access to documentation. Not only to get the obvious answer to «what does this function do?», but also to ask the cleverer «is there a function with this type signature?».
A way to print glyphs for Unicode
String
([Char]
), when I’m dealing with a toy experiment, or libraries that haven’t switched toText
. This would be my «standard» mode of operations, since it’s easier to experiment with UnicodeString
and then switch everything toText
.A way to pretty-print results, when I know them to be complex and need the extra readability. Say long lists of sum types with product types inside. This would be my «alternate» mode of operations.
The result’s general type signature, and runtime statistics for the last evaluated expression.
Lean history management, basic name auto completion, and editing capabilites matching my editor of choice. Keep in mind I usually have a programming editor open, so these editing needs are for whatever I’m trying within GHCi.
Let’s address these needs.
hoogle
searches
Hoogle
is a Haskell API to search
libraries either by explicit function name (e.g. search for map
) or
by approximate type signature (e.g. search for (a -> b) -> [a] -> [b]
).
And it’s possible to use it offline from the command-line.
While having internet access, install the hoogle
executable, and
have it download the latest API collection for all libraries available from
Stackage
.
$ stack install hoogle
(... downloads and builds from source ...)
$ ls -l .local/bin/hoogle
-rwxr-xr-x 1 emhn emhn 42882464 Feb 10 09:07 .local/bin/hoogle
$ hoogle generate
(... downloads ALL THE API's ...)
From that point on, you will have offline immediate access to hoogle
searches over the local database. Just repeat the hoogle generate
every now and then to update the database.
There are two major ways to use it:
«What does a function do?»
$ hoogle --info map map :: (a -> b) -> [a] -> [b] base Prelude map f xs is the list obtained by applying f to each element of xs, i.e., map f [x1, x2, ..., xn] == [f x1, f x2, ..., f xn] map f [x1, x2, ...] == [f x1, f x2, ...] >>> map (+1) [1, 2, 3]
«Are there functions with this particular type signature?»
$ hoogle '(a -> b) -> [a] -> [b]' Prelude map :: (a -> b) -> [a] -> [b] Data.List map :: (a -> b) -> [a] -> [b] GHC.Base map :: (a -> b) -> [a] -> [b] GHC.List map :: (a -> b) -> [a] -> [b] GHC.OldList map :: (a -> b) -> [a] -> [b] Test.Hspec.Discover map :: (a -> b) -> [a] -> [b] Distribution.Compat.Prelude.Internal map :: (a -> b) -> [a] -> [b] Prelude.Compat map :: () => (a -> b) -> [a] -> [b] BasePrelude map :: () => (a -> b) -> [a] -> [b] Data.GI.Base.ShortPrelude map :: (a -> b) -> [a] -> [b] -- plus more results not shown, pass --count=20 to see more
Unicode and Pretty-Printing
The simplest libraries available for these tasks can be installed within the «global project» with
$ stack install utf8-string
$ stack install unicode-show
$ stack install pretty-show
Coming up with a working .ghci
GHCi will read commands from .ghci
on startup. These
commands are either built in GHCi commands as described in
its documentation,
or Haskell expressions that get evaluated. The following
are the current contents of my ~/.ghci
in the same order
they appear.
I start by setting up my preferred external editor and a lean prompt. That’s all I need, really.
:set editor /usr/bin/gvim
:set prompt "λ> "
:set prompt-cont " | "
The type-contextual automatic conversion of literal strings is a frequenly used GHC extension, so I enable it
:set -XOverloadedStrings
You can integrate any external program into GHCi by defining a new GHCi command. These new commands can have any name we like, and be a combination of GHCi builtins alongside Haskell code.
I can run hoogle
from the command line, by virtue of it being
installed in ~/.local/bin/
and that directory being mentioned
in my PATH
. GHCi has the ability to run arbitrary shell
commands using the built-in :!
, so I could run :!hoogle
manually, but passing arguments becomes tedious and tricky.
I rather define a GHCi command that builds a line that will
be evaluated by the shell, and also shell escape the argument
by wrapping it with single quotes («quoting»), and any internal
single quote as well.
let qArg arg = "'" ++ concatMap (\c -> if c == '\'' then "'\"'\"'" else [c]) arg ++ "'"
:def! search pure . (":! hoogle " ++) . qArg
:def! manual pure . (":! hoogle --info " ++) . qArg
First, note that qArg
is a straight Haskell function definition,
but using let
because that’s what GHCi would need. Then, the
:search
command is defined as the expression
pure . (":! hoogle " ++) . qArg
SO, when I type
> :search (a -> b) -> [a] -> [b] ghci
it gets evaluated as
(pure . (":! hoogle " ++) . qArg) "(a -> b) -> [a] -> [b]"
{- (.) definition, twice -}
pure ((":! hoogle " ++) ( qArg "(a -> b) -> [a] -> [b]" ))
{- apply qArg -}
pure ((":! hoogle " ++) "'(a -> b) -> [a] -> [b]'")
{- apply sectioned (++) -}
pure (":! hoogle '(a -> b) -> [a] -> [b]'")
{- produce value in IO context for GHCi -}
":! hoogle '(a -> b) -> [a] -> [b]'"
and then GHCi executes the line: nothing more than built-in :!
,
running hoogle
, its output being displayed
> :search (a -> b) -> [a] -> [b]
ghciPrelude map :: (a -> b) -> [a] -> [b]
Data.List map :: (a -> b) -> [a] -> [b]
GHC.Base map :: (a -> b) -> [a] -> [b]
GHC.List map :: (a -> b) -> [a] -> [b]
GHC.OldList map :: (a -> b) -> [a] -> [b]
Test.Hspec.Discover map :: (a -> b) -> [a] -> [b]
Distribution.Compat.Prelude.Internal map :: (a -> b) -> [a] -> [b]
Prelude.Compat map :: () => (a -> b) -> [a] -> [b]
BasePrelude map :: () => (a -> b) -> [a] -> [b]
Data.GI.Base.ShortPrelude map :: (a -> b) -> [a] -> [b]
-- plus more results not shown, pass --count=20 to see more
It should be easy to add the --count
option to list as many
suggestions as you want. The :manual
command, is the
shortcut to get a function’s documentation
> :manual map
λmap :: (a -> b) -> [a] -> [b]
Prelude
base map f xs is the list obtained by
of xs, i.e.,
applying f to each element
map f [x1, x2, ..., xn] == [f x1, f x2, ..., f xn]
map f [x1, x2, ...] == [f x1, f x2, ...]
>>> map (+1) [1, 2, 3]
As for printing expression results, and being able to go back and forth from my preferred Unicode mode to the optional «pretty printing» mode, I need a couple of commands. Let’s load two of the three libraries above
import Text.Show.Pretty (ppShow)
import Text.Show.Unicode (uprint)
Yes, plain Haskell import
because we’re going to write some
code to build a couple of GHCi commands. These commands require
multiple lines, so we’re going to use GHCi multiline form,
mostly for readability reasons,
:{
:def! pretty const $ pure $ unlines [
":unset +s +t",
"pp = putStrLn . ppShow",
":seti -interactive-print pp",
":set +s +t"
]:}
to define the new GHCI command pretty
taking no arguments.
Thanks to unlines
, all lines in the list will be combined with
a '\n'
in between them, creating a long line with the <ENTER>
s
I would type interactively. The bracketing with :unset
and :set
is
to reduce the «noise» when loading this definition: those flags print
type information and statistics for every command.
The interesting part is function pp
’s definition. Every time
you evaluate a Haskell expression within GHCi, it will try to use
Haskell’s print
over the result, as long as the value’s type has
a Show
instance. Now
ppShow :: Show a => a -> String
takes any value having a Show
instance, and transforms it into
a «pretty printed» String
. Using putStrLn
on that will naturally
print it, conforming to expected GHCi behavior. Finally, setting
interactive-print
to the newly defined function pp
, makes GHCi
use it for printing results, instead of the default print
.
Contrast:
> [1,2,3]
ghci1,2,3]
[> :pretty
ghci> [1,2,3]
ghci1 , 2 , 3 ] [
The definition for unicode
should be easy to follow as well
:{
:def! unicode const $ pure $ unlines [
":unset +s +t",
":seti -interactive-print uprint",
":set +s +t"
]:}
Haskell String
are Unicode strings, but print
does not convert
Unicode entities into the corresponding glyphs, hence the need to
use uprint
. Contrast:
> "toño en acción"
ghci"to\241o en acci\243n"
> :unicode
ghci> "toño en acción"
ghci"toño en acción"
I also have a convenience function to quickly reload ~/.ghci
without needing to restart GHCi. This let’s me make temporary
changes to ~/.ghci
and load it quickly. At this point, it
should be self-explanatory
:def! rr const $ pure ":script ~/.ghci"
The last two commands in my ghci
establish my preferred mode of
printing, and enable resulting type and evaluation statistics
:unicode
:set +s +t
That last line is the one enabling runtime statistics (+s
)
and result value type (+t
) for every evaluated expression, i.e.
> map (*2) $ take 5 $ filter even [1..]
λ4,8,12,16,20]
[it :: Integral b => [b] <-- +t
0.01 secs, 117,760 bytes) <-- +s (
That’s why definitios for :search
and :manual
disable and
reenable them. And since I have a :rr
command for reloading,
then ~/.ghci
’s first line has to be
:unset +s +t
so that I we don’t get messages for every expression being redefined when reloading.
History and editing capabilites
GHCi uses the powerful
Haskeline library
for command-line editing and history management. Haskeline
settings go in ~/.haskeline
and they are actual Haskell values
using Haskeline constructors. My preferences are
: Vi
editMode: menuCompletion
completionType: Just 8
completionPromptLimit: IgnoreConsecutive
historyDuplicates: Nothing maxHistorySize
Vim has
been my preferred programming editor for over 30 years. The above
configuration allows me to work in GHCi and, at any given time
hit <ESC>
to enter vi
-mode, so that I can:
Move up and down command history with
k
andj
(or the arrows)Recall and search history with
Ctrl-R
.Use
vi
line movement or editing keystrokes at will, including yank-and-paste or delete-and-paste.Hit
<TAB>
to get completion based on all names currently in scope.> ma<TAB> λmap mapM mapM_ mappend max maxBound maximum maybe
GHCi history file will be stored at
~/.ghc/ghci_history
. There’s no limit to the number of lines to store there, and consecutive duplicate commands will be stored exactly once.
What else can be done?
Sometimes I disable compiler warnings
:set -w
when working on bringing old-code (pre GHC 9) to modern standards.
That’s the main reason why I added the :rr
command
There’s a lot of placeholders you can add to the prompt, to put things like current working directory, loaded modules, and other things. To me, they are nothing more than a waste of space, and am not willing to have multi-line prompts.
It’s also possible to extend :pretty
to colorize the output using
hscolour.
Not my cup of tea.
I always install HLint
and had an :hlint
command for a while. Nowadays I rather let the
Haskell Language Server
provide suggestions within the dedicated Vim session.