Replacing GHCi's pretty-printer

Published: 2017-02-13
Words: 956

I work in GHCi all day long. When an expression returns a value, GHCi tries to write it out to the terminal using System.IO.print. This is nice when we're printing out a simple, flat expression:

λ> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

... but it's a bit less nice when we're dealing with complicated nested or recursive structures:

λ> import Data.Tree
λ> Node [1..10] [Node [20..30] [], Node [40..50] []]
Node {rootLabel = [1,2,3,4,5,6,7,8,9,10], subForest = [Node {rootLabel = [20,21,22,23,24,25,26,27,28,29,30], subForest = []},Node {rootLabel = [40,41,42,43,44,45,46,47,48,49,50], subForest = []}]}

This is pretty unpleasant. This simple example is nearly 200 characters long, and wraps several times on a regular terminal. We can't easily inspect the nesting level, and it's quite hard to visually scan the result for anomalies.

-interactive-print

Luckily, GHCi is quite configurable! Using the -interactive-print flag, We can replace IO.print as the pretty-printer of choice, and slot in any function we please. My favourite alternative pretty-printer is Iavor Diatchki's wonderful pretty-show, which uses a Haskell lexer. If the lexer succeeds, it uses a Haskell pretty-printer over the tokens; if it fails, it prints the original string.

λ> import Text.Show.Pretty (ppShow, pPrint)
λ> :set -interactive-print pPrint
λ> import Data.Tree
λ> Node [1..10] [Node [20..30] [], Node [40..50] []]
Node
  { rootLabel = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
  , subForest =
      [ Node
          { rootLabel =
              [ 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ]
          , subForest = []
          }
      , Node
          { rootLabel =
              [ 40 , 41 , 42 , 43 , 44 , 45 , 46 , 47 , 48 , 49 , 50 ]
          , subForest = []
          }
      ]
  }

Lovely!

We can also combine pretty-printers. For example, if hscolour is installed, we can pipe ppShow into it to get syntax highlighting for the terminal:

λ> import Text.Show.Pretty (ppShow)
λ> import Language.Haskell.HsColour
λ> import Language.Haskell.HsColour.Colourise
λ> let colorPrint = IO.putStrLn . hscolour TTY defaultColourPrefs False False "" False . ppShow
λ> :set -interactive-print colorPrint

This works nicely:

However, it does not survive :reload. colorPrint is a local definition, so it is cleared on reload along with the rest of our session bindings. If you want your custom pretty-printer to survive long-term, it needs to be a symbol defined in a registered package, such as pretty-show. For this reason, vanilla pPrint is a safer bet.

GHCi macros

Because I almost always have pretty-show available as a transitive dependency, I use some GHCi macros to install pPrint. I invoke them whenever I encounter a wall of text:

:def pretty \_ -> return ("import Text.Show.Pretty (pPrint, ppShow)\n:set -interactive-print pPrint")
:def no-pretty \_ -> return (":set -interactive-print System.IO.print")

You might place these macros in ~/.ghci.conf, or in your project's local ghci.conf file, which sits alongside your cabal file. We can then invoke :pretty at the GHCi prompt, as shorthand to install Text.Show.Pretty.pPrint as our printer. GHCi has lots of configuration options! Here are a few that I find useful.

Keeping pretty-show installed

Just to reiterate, the printer you choose may not always be available. It needs to be defined in an installed and registered package in order to persist. This means it must be installed in your project sandbox, or your user package database (~/.cabal). You can install into the user package database via cabal install pretty-show. Alternatively, be lazy like me, and only use it when it's already a transitive dependency.

Proceed directly to computer gaol

We can install an arbitrary symbol as our pretty-printer. It doesn't have to typecheck, though it does need to be defined:

λ> let foo = const 10
λ> :set -interactive-print foo
λ> 100

<interactive>:20:1:
    No instance for (Num (IO a0)) arising from a use of ‘foo’
    In a stmt of an interactive GHCi command: foo it
λ> :set -interactive-print bar

<interactive>:1:1: Not in scope: ‘bar’

... and this means we can produce outrageous hacks! For example, we could introduce and use an extra constraint, like Ord:

λ> let quux = IO.print . L.sort
λ> :set -interactive-print quux
λ> [5,4,3,2,1]
[1,2,3,4,5]

The possibilities are endless! We can perform arbitrary IO on any value returned by the repl. Here are a few evil ideas for ways to abuse this:

Feature history

This feature has been present since 2012, apparently instigated by Phil Wadler and implemented by Vitaly Bragilevsky. While it's pretty clearly explained in the user guide, I learned of this feature only when I sat down to hack pretty-show into GHCi myself! I hope this post brings it to a few more people.

Prior to GHC 8.0, a bug was present whereby reloading the session would clear the -interactive-print setting. I can confirm this has been fixed and does not affect GHC 8.0.1. Note that you should choose a symbol defined in a registered package to ensure that it survives reloads.

Thanks

Thanks to several discreet and wonderful individuals for proof-reading this post. I don't normally polish my writing very much. However, I'm trying to work on my technical communication, so your friendly scrutiny has been a very welcome change! If you want to be publicly lauded, let me know.