A Month of Haskell, Day 6 - Text.Printf

Posted on May 10, 2017 by Chris Lumens in month-of-haskell.

Here’s a quick one for the sixth entry in this series. The Text.Printf module won’t completely change how you program, but it’s an extremely useful module that needs to be more well known. Because it’s part of the base system, you don’t need to install anything to make use of it.

Printing a string that includes a lot of values in Haskell can be pretty tedious:

import Data.Version(showVersion)
import System.Info

main :: IO ()
main = do
    putStrLn $ os ++ ", running on " ++ arch ++ "\n" ++ "Haskell compiler " ++ compilerName ++
               ", version " ++ showVersion compilerVersion
    return ()

Luckily, the base system includes a module that acts an awful lot like the C printf function (or the shell printf command) which you are likely already very familiar with.

Type signatures:

printf :: PrintfType r => String -> r

Just like the C version, it takes a format string and a variable number of arguments that are converted before being printed. The above could also be written like so:

import Data.Version(showVersion)
import System.Info
import Text.Printf

main :: IO ()
main = do
    printf "%s, running on %s\nHaskell compiler %s, version %s\n"
           os arch compilerName (showVersion compilerVersion)
    return () 

This does what you would expect. Unlike the C version, you can also easily just have printf output to a string that you could later print. You could also do anything else with it that you could do with strings - store in a database, transmit over the network, and so forth.

Also similarly to C, there’s a hPrintf version that works more like fprintf - you also pass it a handle for where it should print to. This version obviously does not allow for printing to a string variable. Printing to the Haskell equivalent of stderr looks like this:

import Data.Version(showVersion)
import System.IO(stderr)
import System.Info
import Text.Printf

main :: IO ()
main = do
    hPrintf stderr "%s, running on %s\nHaskell compiler %s, version %s\n"
                   os arch compilerName (showVersion compilerVersion)
    return ()

The documentation is actually very complete. If you are at all familiar with printf from other languages, you will be able to make sense of its discussion of format characters and precision and field width. I don’t want to go into it here, because it is fairly boring stuff that’s easy to experiment with.

There’s one other very handy thing you can do with printf. Just like everything else in Haskell, it will perform type checking on its arguments. In fact, all arguments passed to printf must be an instance of the PrintfArg type class.

Type classes:

class PrintfArg a where
    formatArg :: a -> FieldFormatter
    parseFormat :: a -> ModifierParser

All the basic stuff is already an instance of this type class. But what if you invented some new string-like type and wanted to make sure it could also be passed as an argument?

Back on day 2 I did exactly that when I invented an upper case string type. This was just like a regular string, but it would automatically convert everything to upper case. It would be very handy to also be able to pass these things to printf. At the time, I glossed right over it but now it’s worth digging into.

All that is necessary to make something an instance of PrintfArg is to define a formatArg function that converts the type into something that printf already understands. Luckily, Text.Printf already defines a bunch of functions like formatString, formatChar, and formatInteger so all you really need to do is convert your new type into one of those and call the appropriate function, and everything will work out.

The instance definition for UpperString looks like this:

instance PrintfArg UpperString where
    formatArg = formatString . getUpperString

The getUpperString function simply extracts the string itself out of the type. That gets passed to the existing formatString function that takes a string and converts it to what printf expects. The following code prints out what you would expect:

main :: IO ()
main = do
    let s = fromString "some text" :: UpperString
    printf  "%s\n" s