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 <> yAnd
run (later show . now x . later show . now y)is equivalent to
\a b -> show a <> x <> show b <> yThe package is available on Hackage as formatting.
Example:
format ("Person's name is " % text % ", age is " % hex) "Dave" 54or with short-names:
format ("Person's name is " % t % ", age is " % x) "Dave" 54Similar 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 mThis 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 cRather 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 -> aAll 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 aFinally, 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 aFor 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 -> aBecause builders are efficient generators.
So in this case we will be expected to produce Builders from arguments:
format . later :: (a -> Builder) -> a -> TextTo 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 :: BuilderComing 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.