text-builder-linear [](https://hackage.haskell.org/package/text-builder-linear) [](http://stackage.org/lts/package/text-builder-linear) [](http://stackage.org/nightly/package/text-builder-linear)

September 14, 2025 · View on GitHub

Linear types for linear times!

Builder for strict Text and ByteString, based on linear types. It consistently outperforms lazy Builder from text as well as a strict builder from text-builder, and scales better.

Example

> :set -XOverloadedStrings
> import Data.Text.Builder.Linear
> fromText "foo" <> fromChar '_' <> fromDec (42 :: Int)
"foo_42"

Design

String builders in Haskell serve the same purpose as StringBuilder in Java to prevent quadratic slow down in concatenation.

Classic builders such as Data.Text.Lazy.Builder are lazy and fundamentally are dlist with bells and whistles: instead of actually concatenating substrings we compose actions, which implement concatenation, building a tree of thunks. The tree can be forced partially, left-to-right, producing chunks of strict Text, combined into a lazy one. Neither input, nor output need to be materialized in full, which potentially allows for fusion. Such builders allow linear time complexity, but constant factors are relatively high, because thunks are expensive. To a certain degree this is mitigated by inlining, which massively reduces number of nodes.

Strict builders such as text-builder offer another design: they first inspect their input in full to determine output length, then allocate a buffer of required size and fill it in one go. If everything inlines nicely, the length may be known in compile time, which gives blazingly fast runtime. In more complex cases it still builds a tree of thunks and forces all inputs to be materialized.

This package offers two interfaces. One is a mutable Buffer with linear API, which operates very similar to StringBuilder in Java. It allocates a buffer with extra space at the ends to append new strings. If there is not enough free space to insert new data, it allocates a twice larger buffer and copies itself there. The dispatch happens in runtime, so we do not need to inspect and materialize all inputs beforehand; and inlining is mostly irrelevant. Exponential growth provides for amortized linear time. Such structure can be implemented without linear types, but that would greatly affect user experience by polluting everything with ST monad. Users are encouraged to use Buffer API, and built-in benchmarks refer to it.

The second interface is more traditional newtype Builder = Builder (Buffer ⊸ Buffer) with Monoid instance. This type provides easy migration from other builders, but may suffer from insufficient inlining, allocating a tree of thunks. It is still significantly faster than Data.Text.Lazy.Builder, as witnessed by benchmarks for blaze-builder below.

Benchmarks for Text

Measured with GHC 9.12 on aarch64:

Group / sizetexttext-builderThis package
Text
163.3 ns30.8 ns0.49x60.5 ns0.95x
10764 ns267 ns0.35x319 ns0.42x
1007.53 μs2.48 μs0.33x2.61 μs0.35x
100080.5 μs26.7 μs0.33x23.1 μs0.29x
10000949 μs319 μs0.34x242 μs0.26x
10000018.5 ms8.22 ms0.44x2.36 ms0.13x
1000000216 ms107 ms0.49x22.9 ms0.11x
Char
149.0 ns34.8 ns0.71x38.1 ns0.78x
10365 ns293 ns0.80x117 ns0.32x
1003.20 μs2.38 μs0.74x804 ns0.25x
100035.4 μs18.4 μs0.52x7.68 μs0.22x
10000460 μs265 μs0.58x86.5 μs0.19x
10000012.7 ms6.96 ms0.55x930 μs0.07x
1000000175 ms178 ms1.02x10.5 ms0.06x
Decimal
1148 ns490 ns3.30x126 ns0.85x
101.34 μs4.80 μs3.57x1.07 μs0.80x
10014.3 μs53.6 μs3.76x10.8 μs0.76x
1000148 μs738 μs5.00x106 μs0.72x
100001.66 ms19.8 ms11.96x1.05 ms0.63x
10000028.3 ms251 ms8.88x10.7 ms0.38x
1000000334 ms2.803 s8.40x108 ms0.32x
Hexadecimal
1711 ns81.2 ns0.11x74.2 ns0.10x
107.06 μs795 ns0.11x510 ns0.07x
10076.3 μs8.04 μs0.11x4.62 μs0.06x
1000862 μs141 μs0.16x44.6 μs0.05x
1000012.4 ms1.73 ms0.14x451 μs0.04x
100000138 ms20.4 ms0.15x4.52 ms0.03x
10000001.502 s228 ms0.15x45.9 ms0.03x
Double
113.4 μs35.3 μs2.63x638 ns0.05x
10137 μs393 μs2.88x6.52 μs0.05x
1001.35 ms5.62 ms4.15x67.9 μs0.05x
100014.2 ms71.9 ms5.05x671 μs0.05x
10000143 ms750 ms5.25x7.18 ms0.05x
1000001.435 s7.941 s5.53x70.4 ms0.05x
100000014.366 s101.342 s7.05x689 ms0.05x

If you are not convinced by synthetic data, here are benchmarks for blaze-markup after migration to Data.Text.Builder.Linear:

bigTable
  992  μs ±  80 μs, 49% less than baseline
basic
  4.35 μs ± 376 ns, 47% less than baseline
wideTree
  1.26 ms ±  85 μs, 53% less than baseline
wideTreeEscaping
  217  μs ± 7.8 μs, 58% less than baseline
deepTree
  242  μs ±  23 μs, 48% less than baseline
manyAttributes
  811  μs ±  79 μs, 58% less than baseline
customAttribute
  1.68 ms ± 135 μs, 56% less than baseline

Benchmarks for ByteString

Somewhat surprisingly, text-builder-linear now offers rendering to strict ByteString as well. It gets consistently faster than bytestring in all benchmarks except Double ones once a string gets over 32k (which is defaultChunkSize for bytestring builder). For mid-sized strings bytestring is slightly faster in certain disciplines, mostly by virtue of using cbits via FFI, while this package remains 100% native Haskell.

Benchmarks below were measured with GHC 9.12 on aarch64 and include comparison to bytestring-strict-builder:

Group / sizebytestring…-strict-builderThis package
Text
1156 ns55.9 ns0.36x60.5 ns0.39x
10552 ns374 ns0.68x319 ns0.58x
1004.71 μs3.25 μs0.69x2.61 μs0.55x
100041.7 μs31.9 μs0.76x23.1 μs0.56x
10000438 μs366 μs0.84x242 μs0.55x
1000007.58 ms6.52 ms0.86x2.36 ms0.31x
1000000112 ms88.1 ms0.78x22.9 ms0.20x
Char
1138 ns30.9 ns0.22x38.1 ns0.28x
10408 ns136 ns0.33x117 ns0.29x
1002.96 μs1.25 μs0.42x804 ns0.27x
100030.4 μs14.2 μs0.47x7.68 μs0.25x
10000394 μs218 μs0.55x86.5 μs0.22x
10000011.8 ms4.00 ms0.34x930 μs0.08x
1000000161 ms112 ms0.69x10.5 ms0.07x
Decimal
1209 ns295 ns1.41x126 ns0.60x
101.10 μs2.67 μs2.43x1.07 μs0.98x
1009.76 μs29.8 μs3.05x10.8 μs1.11x
1000100 μs340 μs3.40x106 μs1.06x
100001.02 ms7.32 ms7.15x1.05 ms1.02x
10000014.6 ms103 ms7.04x10.7 ms0.73x
1000000179 ms1.233 s6.87x108 ms0.60x
Hexadecimal
1131 ns74.2 ns0.57x
10360 ns510 ns1.42x
1002.76 μs4.62 μs1.68x
100028.6 μs44.6 μs1.56x
10000330 μs451 μs1.37x
1000007.30 ms4.52 ms0.62x
1000000103 ms45.9 ms0.45x
Double
1456 ns638 ns1.40x
103.58 μs6.52 μs1.82x
10036.2 μs67.9 μs1.87x
1000367 μs671 μs1.83x
100005.17 ms7.18 ms1.39x
10000059.0 ms70.4 ms1.19x
1000000605 ms689 ms1.14x