PyF: Quasiquotations for a python like interpolated string formater

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain]

Quasiquotations for a python like interpolated string formater.


[Skip to ReadMe]

Properties

Versions0.5.0.0, 0.6.0.0, 0.6.0.1, 0.6.0.2, 0.6.1.0, 0.6.1.1, 0.6.1.1
Change logChangeLog.md
Dependenciesbase (>=4.9 && <5.0), containers (==0.5.*), formatting (>=6.2 && <6.4), haskell-src-meta, megaparsec (>=6.0 && <6.6), template-haskell (>=2.11 && <2.14), text (>=0.11 && <1.3) [details]
LicenseBSD-3-Clause
AuthorGuillaume Bouchard
Maintainerguillaum.bouchard@gmail.com
CategoryText
Source repositoryhead: git clone http://github.com/guibou/PyF
UploadedFri Aug 10 20:46:12 UTC 2018 by guibou

Modules

[Index]

Downloads

Maintainers' corner

For package maintainers and hackage trustees


Readme for PyF-0.6.1.1

[back to package description]

PyF is a Haskell library for string interpolation and formatting.

PyF exposes a quasiquoter f for the Formatting library. The quasiquotation introduces string interpolation and formatting with a mini language inspired from printf and Python.

Quick Start

The following Formatting example:

>>> import Formatting

>>> name = "Dave"
>>> age = 54

>>> format ("Person's name is " % text % ", age is " % hex) name age
"Person's name is Dave, age is 36"

can be written as:

>>> import Formatting
>>> import PyF

>>> name = "Dave"
>>> age = 54

>>> format [f|Person's name is {name}, age is {age:x}|]
"Person's name is Dave, age is 36"

The formatting mini language can represent:

You will need the extension QuasiQuotes, enable it with {-# LANGUAGE QuasiQuotes #-} in top of your source file or with :set -XQuasiQuotes in your ghci session.

Expression to be formatted are referenced by {expression:formatingOptions} where formatingOptions follows the Python format mini-language. It is recommended to read the python documentation, but the Test file as well as this readme contain many examples.

More Examples

Padding

Left < / Right > / Around ^ padding:

>>> name = "Guillaume"
>>> format [f|{name:<11}|]
"Guillaume  "
>>> format [f|{name:>11}|]
"  Guillaume"
>>> format [f|{name:|^13}|]
"||Guillaume||"

Padding inside = the sign:

>>> [fString|{-pi:=10.3}|]
"-    3.142"

Float rounding

>>> format [f|{pi:.2}|]
"3.14"

Binary / Octal / Hex representation (with or without prefix)

>>> v = 31
>>> format [f|Binary: {v:#b}|]
"Binary: 0b11111"
>>> format [f|Octal (no prefix): {age:o}|]
"Octal (no prefix): 37"
>>> format [f|Hexa (caps and prefix): {age:#X}|]
"Hexa (caps and prefix): 0x1F"

Grouping

Using , or _.

>>> [fString|{10 ^ 9 - 1:,}|]
"999,999,999"
>>> [fString|{2 ^ 32  -1:_b}|]
"1111_1111_1111_1111_1111_1111_1111_1111"

Sign handling

Using + to display the positive sign (if any) or to display a space instead:

>>> [fString|{pi:+.3}|]
"+3.142"
>>> [fString|{pi: .3}|]
" 3.142"

0

Preceding the width with a 0 enables sign-aware zero-padding, this is equivalent to inside = padding with a fill char of 0.

>>> [fString{-10:010}|]
-000000010

Sub-expressions

First argument inside the curly braces can be a valid Haskell expression, for example:

>>> format [f|2pi = {2* pi:.2}|]
6.28
>>> format [f|tail "hello" = {tail "hello":->6}|]
"tail \"hello\" = --ello"

However the expression must not contain } or : characters.

Combined

Most options can be combined. This generally leads to totally unreadable format string ;)

>>> format [f|{pi:~>5.2}|]
"~~3.14"

Other quasiquoters

PyF main entry point is f but for convenience some other quasiquoters are provided:

PyF reexport most of Formatting runners, such as format, sformat, formatToString, ...

For example:

>>> [f'|hello {pi.2}|] :: String
"hello 3.14"
>>> :type [fString|hello|]
[Char]

Caveats

Type inference

Type inference with numeric literals can be unreliable if your variables are too polymorphic. A type annotation or the extension ExtendedDefaultRules will help.

>>> v = 10 :: Double
>>> [f|A float: {v}|]
A float: 10

Error reporting

Template haskell is generally known to give developers a lot of frustration when it comes to error message, dumping an unreadable piece of generated code.

However, in PyF, we took great care to provide clear error reporting, this means that:

>>> [f|{age:.3d}|]

<interactive>:77:4: error:
    • <interactive>:1:8:
  |
1 | {age:.3d}
  |        ^
Type incompatible with precision (.3), use any of {'e', 'E', 'f', 'F', 'g', 'G', 'n', 's', '%'} or remove the precision field.
>>> [f|{toto}|]
<interactive>:78:4: error: Variable not in scope: toto
>>*> [fString|{True:d}|]

<interactive>:80:10: error:
    • No instance for (Integral Bool)
        arising from a use of ‘PyF.Internal.QQ.formatAnyIntegral’
...
>>> [fString|{"hello":=10s}|]

<interactive>:88:1: error:
    • Exception when trying to run compile-time code:
        String Cannot be aligned with the inside `=` mode
CallStack (from HasCallStack):
  error, called at src/PyF/Internal/QQ.hs:143:18 in PyF-0.4.0.0-inplace:PyF.Internal.QQ
      Code: quoteExp fString "{\"hello\":=10s}"
    • In the quasi-quotation: [fString|{"hello":=10s}|]

And

*PyF PyF.Internal.QQ> [fString|{"hello":=10}|]

<interactive>:89:10: error:
    • String Cannot be aligned with the inside `=` mode
...
>>> [fString|{3 + pi + "hello":10}|]

<interactive>:99:10: error:
    • No instance for (Floating [Char]) arising from a use of ‘pi’
    ...

Custom Delimiters

If { and } does not fit your needs, for example if you are formatting a lot of json, you can use custom delimiters. All quasi quoters have a parametric form which accepts custom delimiters. Due to template haskell stage restriction, you must define your custom quasi quoter in an other module.

For example, in MyCustomDelimiter.hs:

module MyCustomQQ where

import Language.Haskell.TH.Quote

import PyF

myCustomFormatter :: QuasiQuoter
myCustomFormatter = fStringWithDelimiters ('@','!')

Later, in another module:

import MyCustomQQ

-- ...

[myCustomFormatter|pi = @pi:2.f!|]

Escaping still works by doubling the delimiters, @@!!@@!! will be formatted as @!@!.

Difference with the Python Syntax

The implementation is unit-tested against the reference python implementation (python 3.6.4) and should match its result. However some formatters are not supported or some (minor) differences can be observed.

Not supported

Difference

Build / test

Should work with stack build; stack test, and with cabal and (optionally) nix:

nix-shell # Optional, if you use nix
cabal new-build
cabal new-test

TODO

Library note

PyF.Formatters exposes two functions to format numbers. They are type-safe (as much as possible) and comes with a combination of formatting options not seen in other formatting libraries:

>>> formatIntegral Binary Plus (Just (20, AlignInside, '~')) (Just (4, ',')) 255
"+~~~~~~~~~~1111,1111"

Conclusion

Don't hesitate to make any suggestion, I'll be more than happy to work on it.