MyNixOS website logo
Description

Combinator-based type-safe formatting (like printf() or FORMAT)

Combinator-based type-safe formatting (like printf() or FORMAT), modelled from the HoleyMonoids package.

See the README at https://github.com/AJChapman/formatting#readme for more info.

formatting Build Status Hackage

Formatting is a type-safe and flexible library for formatting text from built-in or custom data types.

Usage

You will probably need the OverloadedStrings language extension, and to import Formatting:

{-# LANGUAGE OverloadedStrings #-}

import Formatting

You may also need some or all of these:

import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TLB

Now a simple example:

> format ("Person's name is " % text % " and age is " % int) "Dave" 54
"Person's name is Dave and age is 54"

In this example, the formatters are two string literals (which take no arguments), and two formatters which take arguments: text, which takes a lazy Text, and int which takes any Integral, such as Int. They are all joined together using the % operator, producing a formatter which takes two arguments: a lazy Text and an Integral. It produces a lazy Text, because we used format. To produce other string types, or print the result instead, refer to this table:

To produce ause
TL.Textformat
T.Textsformat
Builderbformat
StringformatToString

To print the values instead, refer to this table:

To print touse
stdoutfprint
stdout, appending a newlinefprintLn
a handlehprint
a handle, appending a newlinehprintLn

Apart from the % operator, formatters can also be joined using the monoid append operator (<>) to avoid repeating the same argument, they can be chained using %., and there are also formatter combinators for composing more advanced combinators. More on this below.

Formatter Quick Reference

Built-in formatters:

To format ae.g.asuseshort form
lazy Text"Hello""Hello"textt
strict Text"World!""World!"stextst
String"Goodbye""Goodbye"strings
Builder"Bathtub""Bathtub"builder
Show a => a[1, 2, 3]"[1, 2, 3]"shownsh
Char'!'"!"charc
Integral a => a23"23"intd
Real a => a123.32"123.32"floatsf
Real a => a123.32"123.320"fixed3f
Scientificscientific 60221409 16"6.0221409e23"sci
Scientificscientific 60221409 16"6.022e23"scifmtExponent (Just 3)
Buildable n, Integral n => n123456"12.34.56"groupInt2 '.'
Buildable n, Integral n => n12000"12,000"commas
Integral n => n32"32nd"ords
Num a, Eq a => a1"1 ant"int <>plural"ant" "ants"
Num a, Eq a => a2"2 ants"int <>plural"ant" "ants"
Enum a => aa"97"asInt
Integral a => a23"10111"binb
Integral a => a23"0b10111"prefixBin
Integral a => a23"27"octo
Integral a => a23"0o27"prefixOct
Integral a => a23"17"hexx
Integral a => a23"0x17"prefixHex
Integral a => a23"13"base20
Buildable a => a10" 10"left4 ' 'l
Buildable a => a10"10 "right4 ' 'r
Buildable a => a10" 10 "center4 ' '
Buildable a => a123456"123"fitLeft3
Buildable a => a123456"456"fitRight3
Buildable a => aTrue"True"build
aundefined"gronk!"fconst"gronk!"

Formatter Combinator Quick Reference

Formatter combinators take a formatter and modify it somehow, e.g. by using it to format elements of a list, or changing its output.

Built-in formatter combinators:

To format ae.g.asuse
Maybe aNothing"Goodbye"maybed"Goodbye" text
Maybe aJust "Hello""Hello"maybed"Goodbye" text
Maybe aNothing""optionedtext
Maybe aJust "Hello""Hello"optionedtext
Either a bLeft "Error!""Error!"eitheredtext int
Either a bRight 69"69"eitheredtext int
Either a xLeft "bingo""bingo"leftedtext
Either a xRight 16""leftedtext
Either x aRight "bingo""bingo"rightedtext
Either x aLeft 16""rightedtext
Foldable t => t a[1, 2, 3]"1st2nd3rd"concatenatedords
Foldable t => t a[123, 456, 789]"789456123"joinedWith(mconcat . reverse) int
Foldable t => t a[1, 2, 3]"1||2||3"intercalated"||" int
Foldable t => t a[1, 2, 3]"1 2 3"unwordedint
Foldable t => t a[1, 2, 3]"1\n2\n3"unlinedd
Foldable t => t a[1, 2, 3]"1 2 3"spacedint
Foldable t => t a[1, 2, 3]"1,2,3"commaSepint
Foldable t => t a[1, 2, 3]"1st, 2nd, 3rd"commaSpaceSepords
Foldable t => t a["one", "two", "three"]"[one, two, three]"listt
Foldable t => t a["one", "two", "three"]"[\"one\", \"two\", \"three\"]"qlistt
[a][1..]"[1, 10, 11, 100]"took4 (list bin)
[a][1..6]"[4, 5, 6]"dropped3 (list int)
a"one two\tthree\nfour"one, two, three, four"splatisSpace commaSpaceSep stext
a1234567890"[123, 456, 789, 0]"splatWith(chunksOf 3) list int
a"one,two,three""one\ntwo\nthree\n"splatOn"," unlined t
a"one two three ""[one, two, three]"wordedlist text
a"one\n\ntwo\nthree\n\n"["one", "", "two", "three", ""]"linedqlist text
a123456"654321"alteredWithTL.reverse int
a"Data.Char.isUpper"DCU"charsKeptIfisUpper string
a"Data.Char.isUpper"ata.har.ispper"charsRemovedIfisUpper string
a"look and boot""leek and beet"replaced"oo" "ee" text
a"look and boot""LOOK AND BOOT"uppercased
a"Look and Boot""look and boot"lowercased
a"look and boot""Look And Boot"titlecased
a"hellos""he..."ltruncated5 text
a"hellos""h...s"ctruncated
a"hellos""...os"rtruncated5 text
a1" 1"lpadded3 int
a1"1 "rpadded3 int
a1" 1 "cpadded3 int
a123"123 "lfixed4 int
a123456"1..."lfixed4 int
a123" 123"rfixed4 int
a123456"...6"rfixed4 int
a123" 123 "cfixed2 1 ' ' int
a1234567"12...7"cfixed2 1 ' ' int
a"Goo""McGoo"prefixed"Mc" t
a"Goo""Goosen"suffixed"sen" t
a"Goo""McGooMc"surrounded"Mc" t
a"Goo""McGoosen"enclosed"Mc" "sen" t
a"Goo""'Goo'"squotedt
a"Goo""\"Goo\""dquotedt
a"Goo""(Goo)"parenthesisedt
a"Goo""[Goo]"squaredt
a"Goo""{Goo}"bracedt
a"Goo""<Goo>"angledt
a"Goo""`Goo`"backtickedt
a"Goo"" Goo"indented3 t
Foldable t => t a[1, 2, 3]" 1\n 2\n 3"indentedLines2 d
a"1\n2\n3"" 1\n 2\n 3"reindented2 t
Integral i, RealFrac d => d6.66"7"roundedToint
Integral i, RealFrac d => d6.66"6"truncatedToint
Integral i, RealFrac d => d6.66"7"ceilingedToint
Integral i, RealFrac d => d6.66"6"flooredToint
field through a Lens' s a(1, "goo")"goo"viewed_2 t
field through a record accessor s -> a(1, "goo")"1"accessedfst d
Integral a => a4097"0b0001000000000001"binPrefix16
Integral a => a4097"0o0000000000010001"octPrefix16
Integral a => a4097"0x0000000000001001"hexPrefix16
Ord f, Integral a, Fractional f => a1024"1KB"bytesshortest
Ord f, Integral a, Fractional f => a1234567890"1.15GB"bytes(fixed 2)

Composing formatters

%. is like % but feeds one formatter into another:

λ> format (left 2 '0' %. hex) 10
"0a"

Using more than one formatter on the same argument

λ> now <- getCurrentTime
λ> format (year % "/" <> month <> "/" % dayOfMonth) now
"2015/01/27"

The Buildable Typeclass

One of the great things about formatting is that it doesn't rely on typeclasses: you can define one or more formatters for each of your types. But you also have the option of defining a 'default' formatter for a type, by implementing the Buildable typeclass, which has one method: build :: p -> Builder. Once this is defined for a type, you can use the build formatter (which is distinct from the build method of Buildable!):

> format ("Int: " % build % ", Text: " % build) 23 "hello"
"Int: 23, Text: hello"

Note that while this can be convenient, it also sacrifices some type-safety: there's nothing preventing you from putting the arguments in the wrong order, because both Int and Text have a Buildable instance. Note also that if a type already has a Show instance then you can use this instead, by using the shown formatter.

Understanding the Types

Formatters generally have a type like this:

Format r (a -> r)

This describes a formatter that will eventually produce some string type r, and takes an a as an argument. For example:

int :: Integral a => Format r (a -> r)

This takes an Integral a argument, and eventually produces an r. Let's work through using this with format:

-- format has this type:
format :: Format TL.Text a -> a

-- so in 'format int', called with an 'Int', 'int's type specialises to:
int :: Format TL.Text (Int -> TL.Text)

-- and 'format's 'a' parameter specialises to 'Int -> TL.Text':
format :: Format TL.Text (Int -> TL.Text) -> Int -> TL.Text

-- so 'format int' takes an Int and produces text:
format int :: Int -> TL.Text

What can be confusing in the above is that int's a parameter expands to Int, but format's a parameter expands to Int -> TL.Text.

Now let's look at what happens when we use the % operator to append formatters:

-- Here are the types of the functions we will use:
(%) :: Format r a -> Format r' r -> Format r' a
int :: Format r (Int -> r) -- simplified for this use
stext :: Format r (T.Text -> r)

-- Within the call to '%', in the expression 'int % stext', the type parameters expand like this:
-- r = T.Text -> r'
-- a = Int -> T.Text -> r'
-- and so we have these types:
int :: Format (T.Text -> r') (Int -> T.Text -> r')
stext :: Format r' (T.Text -> r')
int % stext :: Format r' (Int -> T.Text -> r')

-- And so when we use 'format' we get a function that takes two arguments and produces text:
format (int % stext) :: Int -> T.Text -> TL.Text

Comparison with Other Languages

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)

Formatter Examples

"Hello, World!": Texts

> format (text % "!") "Hi!"
"Hi!!"
> format (string % "!") "Hi!"
"Hi!!"

123: Integers

> format int 23
"23"

23.4: Decimals

> format (fixed 0) 23.3
"23"
> format (fixed 2) 23.3333
"23.33"
> format shortest 23.3333
"23.3333"
> format shortest 0.0
"0.0"
> format sci 2.3
"2.3"
> format (scifmt Fixed (Just 0)) 2.3
"2"

1,242: Commas

> format commas 123456778
"123,456,778"
> format commas 1234
"1,234"

1st: Ordinals

> format ords 1
"1st"
> format ords 2
"2nd"
> format ords 3
"3rd"
> format ords 4
"4th"

3F: Hex

> format hex 15
"f"
> format hex 25
"19"

Monday 1st June: Dates & times

> now <- getCurrentTime
> later <- getCurrentTime
> format (dayOfMonth % "/" % month % "/" % year) now now now
"16/06/2014"
> format day now
"167"
> format hms now
"08:24:41"
> format tz now
"+0000"
> format datetime now
"Mon Jun 16 08:24:41 UTC 2014"
> format century now
"20"
> format (dayOfMonthOrd % " of " % monthName) now now
"16th of June"

3 years ago: Time spans

> format (diff False) (diffUTCTime later now)
"2 seconds"
> format (diff True) (diffUTCTime later now)
"in 2 seconds"
> format (diff True) (diffUTCTime now later)
"2 seconds ago"
> format (seconds 0 % " secs") (diffUTCTime now later)
"2 secs"
> let Just old = parseTime defaultTimeLocale "%Y" "1980" :: Maybe UTCTime
> format (years 0) (diffUTCTime now old)
"34"
> format (diff True) (diffUTCTime now old)
"in 35 years"
> format (diff True) (diffUTCTime old now)
"35 years ago"
> format (days 0) (diffUTCTime old now)
"12585"
> format (days 0 % " days") (diffUTCTime old now)
"12585 days"

File sizes

> format (bytes shortest) 1024
"1KB"
> format (bytes (fixed 2 % " ")) (1024*1024*5)
"5.00 MB"

Scientific

If you're using a type which provides its own builder, like the Scientific type:

import Data.Text.Lazy.Builder.Scientific
scientificBuilder :: Scientific -> Builder
formatScientificBuilder :: FPFormat -> Maybe Int -> Scientific -> Builder

Then you can use later easily:

> format (later scientificBuilder) 23.4
"23.4"

Actually, there are now already two handy combinators (sci and scifmt) for the Scientific type as shown above in the Decimals section.

Writing your own Formatters

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 -> Builder) -> Format 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 :: Format 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 :: Format 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 :: Integral a => Format r (Maybe a -> 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!"

Using it with other APIs

As a convenience, we provide the FromBuilder typeclass and the formatted combinator. formatted makes it simple to add formatting to any API that is expecting a Builder, a strict or lazy Text, or a String. For example if you have functions logDebug, logWarning and logInfo all of type Text -> IO () you can do the following:

> formatted logDebug ("x is: " % int) x
> formatted logInfo ("y is: " % squared int) y
> formatted logWarning ("z is: " % braced int) z

The above example will work for either strict or lazy Text

Hacking

Building with Nix

See README-nix.md.

Running the Tests

From within your nix-shell, run cabal test.

The tests are in test/Spec.hs.

Running the Benchmarks

Start nix-shell like this: nix-shell --arg doBenchmark true. From within your nix-shell, run cabal bench.

To build the html benchmarking reports, run cabal bench --benchmark-option=-obench/reports/7.2.0.html > bench/reports/7.2.0.txt, replacing '7.2.0' with the current version. This will output the file bench/reports/7.2.0.html which you can open in a browser, and bench/reports/7.2.0.txt which you can view in a terminal or text editor.

The benchmarks are in bench/bench.hs.

Metadata

Version

7.2.0

Platforms (75)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows