proda-ai / formatting / Formatting

A type-safe string formatting library. It fulfils the need for string-interpolation or a printf function, without sacrificing Elm's runtime guarantees or requiring any language-level changes. It also composes well, to make building up complex formatters easy.


type Format r a
    = Format ((String -> r) -> a)

A formatter. This type holds all the information we need to create a formatting function, wrapped up in a way that makes it easy to compose.

Build one of these up with primitives like s, string and int, join them together with |> bs, and when you're done, generate the final printing function with print.

Example

import Formatting exposing (..)

greeting =
    s "Hello " |> bs string |> bs "!"

print greeting "Kris"

--> "Hello Kris!"

Creating Custom Formatters

Imagine you have an existing formatting rule you'd like to turn into a formatter:

tweetSummary : Int -> String -> String
tweetSummary starCount body =
    "(" ++ toString starCount ++ ") " ++ body

First, wrap the type signature in brackets:

tweetSummary : Int -> String -> String

Then change the result type to a variable. (That's where the magic begins - the Formatting library gets control of the final result type.):

tweetSummary : Int -> String -> r

Now add Format r to the start.

tweetSummary : Format r (Int -> String -> r)

All very mechanical. Now for the function body. Let's recall what it looked like at the start:

tweetSummary starCount body =
    "(" ++ toString starCount ++ ") " ++ body

Change that into an anonymous function:

tweetSummary =
    \starCount body ->
        "(" ++ toString starCount ++ ") " ++ body

Now add in a callback function as the first argument:

tweetSummary =
    \callback starCount body ->
        "(" ++ toString starCount ++ ") " ++ body

Pass your function's result to that callback (using <| is the easy way):

tweetSummary =
    \callback starCount body ->
        callback <| "(" ++ toString starCount ++ ") " ++ body

Finally, wrap that all up in a Format constructor:

tweetSummary =
    Format
        (\callback starCount body ->
            callback <| "(" ++ toString starCount ++ ") " ++ body
        )

And you're done. You have a composable formatting function. It's a mechanical process that's probably a bit weird at first, but easy to get used to.

Format (\callback -> g (\strG -> f (\strF -> callback (strG ++ strF))))

bs : Format b a -> Format a c -> Format b c

Compose two formatters together.

map : (String -> String) -> Format r a -> Format r a

Create a new formatter by applying a function to the output of this formatter.

For example:

import String exposing (toUpper)

format =
    s "Name: " |> bs map toUpper string

...produces a formatter that uppercases the name:

print format "Kris"

--> "Name: KRIS"

premap : (a -> b) -> Format r (b -> v) -> Format r (a -> v)

Create a new formatter by applying a function to the input of this formatter. The dual of map.

For example:

format =
    s "Height: " |> bs premap .height float

...produces a formatter that accesses a .height record field:

print format { height: 1.72 }

--> "Height: 1.72"

toFormatter : (a -> String) -> Format r (a -> r)

Convert an ordinary 'stringifying' function into a Formatter.

apply : Format r (a -> b -> r) -> a -> Format r (b -> r)

Apply an argument to a Formatter. Useful when you want to supply an argument, but don't yet want to convert your formatter to a plain ol' function (with print).

print : Format String a -> a

Turn your formatter into a function that's just waiting for its arguments.

Given this format:

orderFormat =
    s "FREE: " |> bs int |> bs s " x " |> bs string |> bs s "!"

...we can either use it immediately:

order : String
order =
    print orderFormat 2 "Ice Cream"

--> "FREE: 2 x Ice Cream!"

...or turn it into an ordinary function to be used later:

orderFormatter : Int -> String -> String
orderFormatter =
    print orderFormat


...elsewhere...


order : String
order = orderFormatter 2 "Ice Cream"

--> "FREE: 2 x Ice Cream!"

html : Format (Html msg) a -> a

Convenience function. Like print, but returns an Html.text node as its final result, instead of a String.

Hint: If you're using any formatters where whitespace is sigificant, you might well need one or both of these CSS rules:

font-family: monospace;
white-space: pre;

s : String -> Format r r

A boilerplate string.

string : Format r (String -> r)

A placeholder for a String argument.

int : Format r (Basics.Int -> r)

A placeholder for an Int argument.

bool : Format r (Basics.Bool -> r)

A placeholder for an Bool argument.

float : Format r (Basics.Float -> r)

A placeholder for a Float argument.

wrap : String -> Format r a -> Format r a

wrap one string with another. It's convenient for building strings like `"Invalid key ''." For example:

print (wrap "'" string) "tester"

--> "'tester'"

pad : Basics.Int -> Char -> Format r a -> Format r a

String.pad lifted into the world of Formatters.

For example:

print (pad 10 '-' string) "KRIS"

--> "---KRIS---"

padLeft : Basics.Int -> Char -> Format r a -> Format r a

String.padLeft lifted into the world of Formatters.

For example:

print (padLeft 10 '_' float) 1.72

--> "______1.72"

padRight : Basics.Int -> Char -> Format r a -> Format r a

String.padRight lifted into the world of Formatters.

For example:

print (padRight 10 '.' int) 789

--> "789......."

dp : Basics.Int -> Format r (Basics.Float -> r)

DEPRECATED: Use roundTo instead.

roundTo : Basics.Int -> Format r (Basics.Float -> r)

A float rounded to n decimal places.

uriFragment : Format r (String -> r)

Format a URI fragment.

For example:

print uriFragment "this string"

--> "this%20string"