Gleam String Builder

The gleam/string_builder module (also available as gleam/string_tree in newer versions) builds large strings efficiently by deferring concatenation. When you join strings with <> repeatedly, each join allocates a new string. A string builder accumulates pieces and concatenates them all at once at the end — significantly faster for large outputs.

The Problem With Repeated Concatenation


Naive concatenation — expensive:
──────────────────────────────────────────────────
let result = "header" <> "\n"
let result = result <> "line 1" <> "\n"
let result = result <> "line 2" <> "\n"
let result = result <> "line 3" <> "\n"
let result = result <> "footer"

Each <> creates a new string in memory.
For 1000 lines: ~1000 allocations.

With string builder — efficient:
──────────────────────────────────────────────────
All pieces stored as a tree.
One final allocation when converting to String.

Creating a String Builder

import gleam/string_builder as sb

// Start from a string
let builder = sb.from_string("Hello")

// Start empty
let empty = sb.new()

Appending and Prepending

import gleam/string_builder as sb

let result =
  sb.new()
  |> sb.append("First line\n")
  |> sb.append("Second line\n")
  |> sb.append("Third line\n")
  |> sb.to_string

// "First line\nSecond line\nThird line\n"

String Builder Tree Structure
──────────────────────────────────────────────────
append("First\n")  → [node: "First\n"]
append("Second\n") → [node: "First\n", node: "Second\n"]
append("Third\n")  → [node: "First\n", node: "Second\n", node: "Third\n"]

to_string() → joins all nodes in one pass
            → "First\nSecond\nThird\n"

Joining a List of Strings

import gleam/string_builder as sb
import gleam/list

let words = ["Gleam", "is", "fast", "and", "safe"]

let sentence =
  words
  |> list.map(sb.from_string)
  |> sb.join(" ")     // joins with spaces
  |> sb.to_string

// "Gleam is fast and safe"

Building HTML Efficiently

import gleam/string_builder as sb

pub fn render_list(items: List(String)) -> String {
  let rows =
    items
    |> list.map(fn(item) {
         sb.from_strings(["
  • ", item, "
  • "]) }) |> sb.join("\n") sb.new() |> sb.append("
      \n") |> sb.append_builder(rows) |> sb.append("\n
    ") |> sb.to_string }
    
    render_list(["Apple", "Banana", "Cherry"])
    ──────────────────────────────────────────────────
    
    • Apple
    • Banana
    • Cherry

    String Builder vs String Concatenation

    
    Performance Comparison
    ──────────────────────────────────────────────────
    Items │ Concatenation (<>)  │ String Builder
    ──────┼─────────────────────┼──────────────────
       10 │ Fast (no diff)      │ Fast
      100 │ Noticeable          │ Fast
     1000 │ Slow                │ Fast
    10000 │ Very slow           │ Fast
    
    Rule of thumb:
      < 20 strings  → use <> directly
      ≥ 20 strings  → use string builder
    

    Common String Builder Functions

    
    string_builder Module Reference
    ──────────────────────────────────────────────────
    sb.new()                  → empty builder
    sb.from_string(s)         → builder from a String
    sb.from_strings([s1, s2]) → builder from list of Strings
    sb.append(builder, s)     → add String to end
    sb.prepend(builder, s)    → add String to start
    sb.append_builder(b1, b2) → combine two builders
    sb.join(builders, sep)    → join list of builders with separator
    sb.to_string(builder)     → convert to final String
    sb.byte_size(builder)     → byte length without converting
    sb.is_empty(builder)      → check if empty
    sb.split(builder, sep)    → split on separator
    sb.reverse(builder)       → reverse content
    sb.lowercase(builder)     → all lowercase
    sb.uppercase(builder)     → all uppercase
    

    Practical Example — CSV Generator

    import gleam/string_builder as sb
    import gleam/list
    import gleam/int
    
    type Row {
      Row(name: String, age: Int, city: String)
    }
    
    pub fn to_csv(rows: List(Row)) -> String {
      let header = sb.from_string("name,age,city\n")
    
      let body =
        rows
        |> list.map(fn(row) {
             sb.from_strings([
               row.name, ",",
               int.to_string(row.age), ",",
               row.city
             ])
           })
        |> sb.join("\n")
    
      sb.new()
      |> sb.append_builder(header)
      |> sb.append_builder(body)
      |> sb.to_string
    }
    
    pub fn main() {
      let data = [
        Row("Alice", 30, "Delhi"),
        Row("Bob",   25, "Mumbai"),
        Row("Carol", 35, "Bangalore")
      ]
      io.println(to_csv(data))
    }
    
    // Output:
    // name,age,city
    // Alice,30,Delhi
    // Bob,25,Mumbai
    // Carol,35,Bangalore
    

    String Tree (Newer API)

    In recent versions of gleam_stdlib, the module is called gleam/string_tree and the type is StringTree. Both string_builder and string_tree work the same way — the API is identical:

    import gleam/string_tree as st
    
    let tree =
      st.new()
      |> st.append("Hello, ")
      |> st.append("World!")
      |> st.to_string
    // "Hello, World!"
    

    Key Points

    
    String Builder Essentials
    ──────────────────────────────────────────────────
    1. Use when building strings from many pieces (20+)
    2. sb.new() or sb.from_string() to start
    3. sb.append() and sb.prepend() add content
    4. sb.join() combines a list with a separator
    5. sb.to_string() finalizes — one allocation
    6. Immutable — every operation returns a new builder
    7. gleam/string_tree is the newer name for the same API
    8. No performance difference for small strings — use <> then
    

    String builders turn an O(n²) problem into an O(n) one. For generating reports, HTML, CSV, JSON, or any template where you combine many pieces of text, a string builder produces the result faster and with less memory pressure than repeated <> concatenation.

    Leave a Comment

    Your email address will not be published. Required fields are marked *