This is how I GHCi

Posted on 2025-02-15 by Ernesto Hernández-Novich
Tags:

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 to Text. This would be my «standard» mode of operations, since it’s easier to experiment with Unicode String and then switch everything to Text.

  • 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

ghci> :search (a -> b) -> [a] -> [b]

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

ghci> :search (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

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]
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]

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:

ghci> [1,2,3]
[1,2,3]
ghci> :pretty
ghci> [1,2,3]
[ 1 , 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:

ghci> "toño en acción"
"to\241o en acci\243n"
ghci> :unicode
ghci> "toño en acción"
"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

editMode: Vi
completionType: menuCompletion
completionPromptLimit: Just 8
historyDuplicates: IgnoreConsecutive
maxHistorySize: Nothing

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 and j (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.