This post is about the formatting package.
The Text.Printf module is problematic simply because it’s not type-safe:
λ> import Text.Printf
λ> printf "" 2
*** Exception: printf: formatting string ended prematurely
λ> printf "%s" 2
*** Exception: printf: bad formatting char 's'
And it’s not extensible in the argument type. The PrintfType class does not export its methods.
And it’s not extensible in the formatting. You can’t add a “%K” syntax to it to print a value in Kelvins, for example.
And it’s implicit. You can’t just use your normal API searching facilities to search how to print a Day.
A while ago I was inspired by the HoleyMonoid package to use that mechanism to make a general replacement for printf.
It’s a continuation-based way of building up monoidal functions by composition with the ability to insert constants in-between. Example:
let holey = now "x = "
. later show
. now ", y = "
. later show
> run holey 3 5
"x = 3, y = 5"
The now function inserts a monoidal value directly into the composition. So
run (now x . now y)
is equivalent to
x <> y
And
run (later show . now x . later show . now y)
is equivalent to
\a b -> show a <> x <> show b <> y
The package is available on Hackage as formatting.
Example:
format ("Person's name is " % text % ", age is " % hex) "Dave" 54
or with short-names:
format ("Person's name is " % t % ", age is " % x) "Dave" 54
Similar to C’s printf:
printf("Person's name is %s, age is %x","Dave",54);
and Common Lisp’s FORMAT:
(format nil "Person's name is ~a, age is ~x" "Dave" 54)
newtype HoleyT r a m = Holey { runHM :: (m -> r) -> a }
type Holey m r a = HoleyT r a m
This is my version of the HoleyMonoid. To make this into a useful package I changed a few things.
The Category instance implied a name conflict burden with (.), so I changed that to (%):
(%) :: Monoid n => Holey n b c -> Holey n b1 b -> Holey n b1 c
Rather than have the name-conflicting map function, I flipped the type arguments of the type and made it an instance of Functor.
There is an array of top-level printing functions for various output types:
-- | Run the formatter and return a lazy 'Text' value.
format :: Holey Builder Text a -> a
-- | Run the formatter and return a strict 'S.Text' value.
sformat :: Holey Builder S.Text a -> a
-- | Run the formatter and return a 'Builder' value.
bprint :: Holey Builder Builder a -> a
-- | Run the formatter and print out the text to stdout.
fprint :: Holey Builder (IO ()) a -> a
-- | Run the formatter and put the output onto the given 'Handle'.
hprint :: Handle -> Holey Builder (IO ()) a -> a
All the combinators work on a lazy text Builder which has good appending complexity and can output to a handle in chunks.
There is a short-hand type for any formatter:
type Format a = forall r. Holey Builder r (a -> r)
All formatters are written in terms of now or later.
There is a standard set of formatters in Formatting.Formatters, for example:
text :: Format Text
int :: Integral a => Format a
sci :: Format Scientific
hex :: Integral a => Format a
Finally, there is a general build function that will build anything that is an instance of the Build class from the text-format package:
build :: Buildable a => Format a
For which there are a bunch of instances. See the README for a full set of examples.
%. is like % but feeds one formatter into another:
λ> format (left 2 '0' %. hex) 10
"0a"
You can include things verbatim in the formatter:
> format (now "This is printed now.")
"This is printed now."
Although with OverloadedStrings you can just use string literals:
> format "This is printed now."
"This is printed now."
You can handle things later which makes the formatter accept arguments:
> format (later (const "This is printed later.")) ()
"This is printed later."
The type of the function passed to later should return an instance of Monoid.
later :: (a -> m) -> Holey m r (a -> r)
The function you format with (format, bprint, etc.) will determine the monoid of choice. In the case of this library, the top-level formating functions expect you to build a text Builder:
format :: Holey Builder Text a -> a
Because builders are efficient generators.
So in this case we will be expected to produce Builders from arguments:
format . later :: (a -> Builder) -> a -> Text
To do that for common types you can just re-use the formatting library and use bprint:
> :t bprint
bprint :: Holey Builder Builder a -> a
> :t bprint int 23
bprint int 23 :: Builder
Coming back to later, we can now use it to build our own printer combinators:
> let mint = later (maybe "" (bprint int))
> :t mint
mint :: Holey Builder r (Maybe Integer -> r)
Now mint is a formatter to show Maybe Integer:
> format mint (readMaybe "23")
"23"
> format mint (readMaybe "foo")
""
Although a better, more general combinator might be:
> let mfmt x f = later (maybe x (bprint f))
Now you can use it to maybe format things:
> format (mfmt "Nope!" int) (readMaybe "foo")
"Nope!"
I’ve been using formatting in a bunch of projects since writing it. Happily, its API has been stable since releasing with some additions.
It has the same advantages as Parsec. It’s a combinator-based mini-language with all the same benefits.