Data.Text.Builder.Linear.Buffer

text-builder-linear-0.1.3: Builder for Text and ByteString based on linear types

Copyright | (c) 2022 Andrew Lelechenko (c) 2023 Pierre Le Marre
License | BSD3
Maintainer | Andrew Lelechenko <andrew.lelechenko@gmail.com>
Safe Haskell | None
Language | GHC2021

Data.Text.Builder.Linear.Buffer

Contents

- Type
- Basic interface
- Single character
- Multiple characters
  - Character replication
  - Text
  - Raw `Addr#`
- Padding
- Number formatting
  - Decimal
    - Bounded numbers
    - Unbounded numbers
  - Hexadecimal
    - Lower-case
    - Upper-case and padding
  - Double

Description

`Buffer` for strict `Text`, based on linear types.

Synopsis

- data Buffer :: UnliftedType
- runBuffer :: (Buffer %1 -> Buffer) %1 -> Text
- runBufferBS :: (Buffer %1 -> Buffer) %1 -> ByteString
- dupBuffer :: Buffer %1 -> (# Buffer, Buffer #)
- consumeBuffer :: Buffer %1 -> ()
- eraseBuffer :: Buffer %1 -> Buffer
- foldlIntoBuffer :: (Buffer %1 -> a -> Buffer) -> Buffer %1 -> [a] -> Buffer
- newEmptyBuffer :: Buffer %1 -> (# Buffer, Buffer #)
- (><) :: Buffer %1 -> Buffer %1 -> Buffer
- (|>.) :: Buffer %1 -> Char -> Buffer
- (.<|) :: Char -> Buffer %1 -> Buffer
- prependChars :: Word -> Char -> Buffer %1 -> Buffer
- appendChars :: Word -> Char -> Buffer %1 -> Buffer
- (|>) :: Buffer %1 -> Text -> Buffer
- (<|) :: Text -> Buffer %1 -> Buffer
- (|>…) :: Buffer %1 -> Word -> Buffer
- (…<|) :: Word -> Buffer %1 -> Buffer
- (|>#) :: Buffer %1 -> Addr# -> Buffer
- (#<|) :: Addr# -> Buffer %1 -> Buffer
- (<|#) :: Addr# -> Buffer %1 -> Buffer
- justifyLeft :: Word -> Char -> Buffer %1 -> Buffer
- justifyRight :: Word -> Char -> Buffer %1 -> Buffer
- center :: Word -> Char -> Buffer %1 -> Buffer
- (|>$) :: (Integral a, FiniteBits a) => Buffer %1 -> a -> Buffer
- ($<|) :: (Integral a, FiniteBits a) => a -> Buffer %1 -> Buffer
- (|>$$) :: Integral a => Buffer %1 -> a -> Buffer
- ($$<|) :: Integral a => a -> Buffer %1 -> Buffer
- (|>&) :: (Integral a, FiniteBits a) => Buffer %1 -> a -> Buffer
- (&<|) :: (Integral a, FiniteBits a) => a -> Buffer %1 -> Buffer
- (|>%) :: Buffer %1 -> Double -> Buffer
- (%<|) :: Double -> Buffer %1 -> Buffer

# Type

data Buffer :: UnliftedType #

Internally `Buffer` is a mutable buffer. If a client gets hold of a variable of type `Buffer`, they'd be able to pass a mutable buffer to concurrent threads. That's why API below is carefully designed to prevent such possibility: clients always work with linear functions `Buffer` ⊸ `Buffer` instead and run them on an empty `Buffer` to extract results.

In terms of `linear-base` `Buffer` is `Consumable` (see `consumeBuffer`) and `Dupable` (see `dupBuffer`), but not `Movable`.

    >>> :set -XOverloadedStrings -XLinearTypes
    >>> import Data.Text.Builder.Linear.Buffer
    >>> runBuffer (\b -> '!' .<| "foo" <| (b |> "bar" |>. '.'))
    "!foobar."

Remember: this is a strict builder, so on contrary to Data.Text.Lazy.Builder for optimal performance you should use strict left folds instead of lazy right ones.

`Buffer` is an unlifted datatype, so you can put it into an unboxed tuple `(# ..., ... #)`, but not into `(..., ...)`.

# Basic interface

runBuffer :: (Buffer %1 -> Buffer) %1 -> Text #

Run a linear function on an empty `Buffer`, producing a strict `Text`.

Be careful to write `runBuffer (\b -> ...)` instead of `runBuffer $ \b -> ...`, because current implementation of linear types lacks special support for `($)`. Another option is to enable `{-# LANGUAGE BlockArguments #-}` and write `runBuffer \b -> ...`. Alternatively, you can import `($)` from `linear-base`.

`runBuffer` is similar in spirit to mutable arrays API in `Data.Array.Mutable.Linear`, which provides functions like `fromList` ∷ [`a`] → (`Vector` `a` ⊸ `Ur` b) ⊸ `Ur` `b`. Here the initial buffer is always empty and `b` is `Text`. Since `Text` is `Movable`, `Text` and `Ur` `Text` are equivalent.

runBufferBS :: (Buffer %1 -> Buffer) %1 -> ByteString #

Same as `runBuffer`, but returning a UTF-8 encoded strict `ByteString`.

dupBuffer :: Buffer %1 -> (# Buffer, Buffer #) #

Duplicate builder. Feel free to process results in parallel threads. Similar to `Dupable` from `linear-base`.

It is a bit tricky to use because of current limitations of linear types with regards to `let` and `where`. E. g., one cannot write

    let (# b1, b2 #) = dupBuffer b in ("foo" <| b1) >< (b2 |> "bar")

Instead write:

    >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples
    >>> import Data.Text.Builder.Linear.Buffer
    >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))
    "foobar"

Note the unboxed tuple: `Buffer` is an unlifted datatype, so it cannot be put into `(..., ...)`.

consumeBuffer :: Buffer %1 -> () #

Consume buffer linearly, similar to `Consumable` from `linear-base`.

eraseBuffer :: Buffer %1 -> Buffer #

Erase buffer's content, replacing it with an empty `Text`.

foldlIntoBuffer :: (Buffer %1 -> a -> Buffer) -> Buffer %1 -> [a] -> Buffer #

This is just a normal `foldl'`, but with a linear arrow and unlifted accumulator.

newEmptyBuffer :: Buffer %1 -> (# Buffer, Buffer #) #

Create an empty `Buffer`.

The first `Buffer` is the input and the second is a new empty `Buffer`.

This function is needed in some situations, e.g. with `justifyRight`. The following example creates a utility function that justify a text and then append it to a buffer.

    >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples
    >>> import Data.Text.Builder.Linear.Buffer
    >>> import Data.Text (Text)
    >>> :{
    appendJustified :: Buffer %1 -> Text -> Buffer
    appendJustified b t = case newEmptyBuffer b of
      -- Note that we need to create a new buffer from the text, in order
      -- to justify only the text and not the input buffer.
      (# b', empty #) -> b' >< justifyRight 12 ' ' (empty |> t)
    :}

    >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "AAA" `appendJustified` "BBBBBBB")
    "Test:         AAA     BBBBBBB"

Note: a previous buffer is necessary in order to create an empty buffer with the same characteristics.

(><) :: Buffer %1 -> Buffer %1 -> Buffer infix 6 #

Concatenate two `Buffer`s, potentially mutating both of them.

You likely need to use `dupBuffer` to get hold on two builders at once:

    >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples
    >>> import Data.Text.Builder.Linear.Buffer
    >>> runBuffer (\b -> case dupBuffer b of (# b1, b2 #) -> ("foo" <| b1) >< (b2 |> "bar"))
    "foobar"

# Single character

(|>.) :: Buffer %1 -> Char -> Buffer infixl 6 #

Append `Char` to a `Buffer` by mutating it.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> b |>. 'q' |>. 'w')
    "qw"

Warning: In contrast to `singleton`, it is the responsibility of the caller to sanitize surrogate code points with `safe`.

(.<|) :: Char -> Buffer %1 -> Buffer infixr 6 #

Prepend `Char` to a `Buffer` by mutating it.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> 'q' .<| 'w' .<| b)
    "qw"

Warning: In contrast to `singleton`, it is the responsibility of the caller to sanitize surrogate code points with `safe`.

# Multiple characters

## Character replication

prependChars :: Word -> Char -> Buffer %1 -> Buffer #

Prepend a given count of a `Char` to a `Buffer`.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> prependChars 3 'x' (b |>. 'A'))
    "xxxA"

appendChars :: Word -> Char -> Buffer %1 -> Buffer #

Apppend a given count of a `Char` to a `Buffer`.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> appendChars 3 'x' (b |>. 'A'))
    "Axxx"

## Text

(|>) :: Buffer %1 -> Text -> Buffer infixl 6 #

Append `Text` suffix to a `Buffer` by mutating it. If a suffix is statically known, consider using `(|>#)` for optimal performance.

    >>> :set -XOverloadedStrings -XLinearTypes
    >>> runBuffer (\b -> b |> "foo" |> "bar")
    "foobar"

(<|) :: Text -> Buffer %1 -> Buffer infixr 6 #

Prepend `Text` prefix to a `Buffer` by mutating it. If a prefix is statically known, consider using `(#<|)` for optimal performance.

    >>> :set -XOverloadedStrings -XLinearTypes
    >>> runBuffer (\b -> "foo" <| "bar" <| b)
    "foobar"

(|>…) :: Buffer %1 -> Word -> Buffer infixr 6 #

Append given number of spaces.

(…<|) :: Word -> Buffer %1 -> Buffer infixr 6 #

Prepend given number of spaces.

## Raw `Addr#`

(|>#) :: Buffer %1 -> Addr# -> Buffer infixl 6 #

Append a null-terminated UTF-8 string to a `Buffer` by mutating it. E. g.,

    >>> :set -XOverloadedStrings -XLinearTypes -XMagicHash
    >>> runBuffer (\b -> b |># "foo"# |># "bar"#)
    "foobar"

The literal string must not contain zero bytes `\NUL` and must be a valid UTF-8, these conditions are not checked.

(#<|) :: Addr# -> Buffer %1 -> Buffer infixr 6 #

Prepend a null-terminated UTF-8 string to a `Buffer` by mutating it. E. g.,

    >>> :set -XOverloadedStrings -XLinearTypes -XMagicHash
    >>> runBuffer (\b -> "foo"# #<| "bar"# #<| b)
    "foobar"

The literal string must not contain zero bytes `\NUL` and must be a valid UTF-8, these conditions are not checked.

Note: When the syntactic extensions `UnboxedTuples` or `UnboxedSums` are enabled, extra spaces are required when using parentheses: i.e. use `( `#<|` )` instead of `(`#<|`)`. See the GHC User Guide chapter “Unboxed types and primitive operations” for further information.

(<|#) :: Addr# -> Buffer %1 -> Buffer infixr 6 #

Deprecated: Use `(#<|)` instead

Alias for ``(#<|)``.

# Padding

justifyLeft :: Word -> Char -> Buffer %1 -> Buffer #

Pad a builder from the right side to the specified length with the specified character.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> justifyLeft 10 'x' (appendChars 3 'A' b))
    "AAAxxxxxxx"
    >>> runBuffer (\b -> justifyLeft 5 'x' (appendChars 6 'A' b))
    "AAAAAA"

Note that `newEmptyBuffer` is needed in some situations. See `justifyRight` for an example.

justifyRight :: Word -> Char -> Buffer %1 -> Buffer #

Pad a builder from the left side to the specified length with the specified character.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> justifyRight 10 'x' (appendChars 3 'A' b))
    "xxxxxxxAAA"
    >>> runBuffer (\b -> justifyRight 5 'x' (appendChars 6 'A' b))
    "AAAAAA"

Note that `newEmptyBuffer` is needed in some situations. The following example creates a utility function that justify a text and then append it to a buffer.

    >>> :set -XOverloadedStrings -XLinearTypes -XUnboxedTuples
    >>> import Data.Text.Builder.Linear.Buffer
    >>> import Data.Text (Text)
    >>> :{
    appendJustified :: Buffer %1 -> Text -> Buffer
    appendJustified b t = case newEmptyBuffer b of
      -- Note that we need to create a new buffer from the text, in order
      -- to justify only the text and not the input buffer.
      (# b', empty #) -> b' >< justifyRight 12 ' ' (empty |> t)
    :}

    >>> runBuffer (\b -> (b |> "Test:") `appendJustified` "AAA" `appendJustified` "BBBBBBB")
    "Test:         AAA     BBBBBBB"

center :: Word -> Char -> Buffer %1 -> Buffer #

Center a builder to the specified length with the specified character.

    >>> :set -XLinearTypes
    >>> runBuffer (\b -> center 10 'x' (appendChars 3 'A' b))
    "xxxxAAAxxx"
    >>> runBuffer (\b -> center 5 'x' (appendChars 6 'A' b))
    "AAAAAA"

Note that `newEmptyBuffer` is needed in some situations. See `justifyRight` for an example.

# Number formatting

## Decimal

### Bounded numbers

(|>$) :: (Integral a, FiniteBits a) => Buffer %1 -> a -> Buffer infixl 6 #

Append the decimal representation of a bounded integral number.

($<|) :: (Integral a, FiniteBits a) => a -> Buffer %1 -> Buffer infixr 6 #

Prepend the decimal representation of a bounded integral number.

### Unbounded numbers

(|>$$) :: Integral a => Buffer %1 -> a -> Buffer infixl 6 #

Append the decimal representation of an unbounded integral number.

Since: 0.1.3

($$<|) :: Integral a => a -> Buffer %1 -> Buffer infixr 6 #

Prepend the decimal representation of an unbounded integral number.

Since: 0.1.3

## Hexadecimal

### Lower-case

(|>&) :: (Integral a, FiniteBits a) => Buffer %1 -> a -> Buffer infixl 6 #

Append the lower-case hexadecimal representation of a bounded integral number.

Negative numbers are interpreted as their corresponding unsigned number:

    >>> :set -XOverloadedStrings -XLinearTypes
    >>> import Data.Int (Int8, Int16)
    >>> runBuffer (\b -> b |>& (-1 :: Int8)) == "ff"
    True
    >>> runBuffer (\b -> b |>& (-1 :: Int16)) == "ffff"
    True

(&<|) :: (Integral a, FiniteBits a) => a -> Buffer %1 -> Buffer infixr 6 #

Prepend the lower-case hexadecimal representation of a bounded integral number.

Negative numbers are interpreted as their corresponding unsigned number:

    >>> :set -XOverloadedStrings -XLinearTypes
    >>> import Data.Int (Int8, Int16)
    >>> runBuffer (\b -> (-1 :: Int8) &<| b) == "ff"
    True
    >>> runBuffer (\b -> (-1 :: Int16) &<| b) == "ffff"
    True

### Upper-case and padding

Note that neither upper case nor padded hexadecimal formatting is provided. This package provides a minimal API with utility functions only for common cases. For other use cases, please adapt the code of this package, e.g. as shown in the Unicode code point example.

## Double

(|>%) :: Buffer %1 -> Double -> Buffer infixl 6 #

Append the decimal representation of a `Double`.

Matches `show` in displaying in standard or scientific notation:

    >>> runBuffer (\b -> b |>% 123.456)
    "123.456"

    >>> runBuffer (\b -> b |>% 1.23e7)
    "1.23e7"

(%<|) :: Double -> Buffer %1 -> Buffer infixr 6 #

Prepend the decimal representation of a `Double`.

Matches `show` in displaying in standard or scientific notation (see examples in ``(|>%)``).