Box combinators

  2025-06-01

A combinator is a function which builds program fragments from program fragments.

In functional programming, combinator libraries refer to a design style that emphasizes bottom-up program construction. Such libraries define a few core data types and provide constructors—functions that create initial objects—and combinators—functions that build larger objects from smaller pieces.

Combinators enable the programmer to use intuitive visual and spatial reasoning that’s vastly more powerful than linear language processing. As a result, solving problems with combinators feels like playing with lego pieces.

This article describes a combinator library that deals with two-dimensional blocks of ascii characters. I’ll use OCaml to demonstrate the ideaFunctional languages are a perfect medium for combinator libraries., but you won’t have trouble translating it to any modern language. Box combinators are my go-to tool when I need to visualize data programmatically for debugging or exploration.

Text boxes

I stumbled on the idea of box combinators around 2012 while reading chapter 10 of Programming in Scala, 2nd edition. The chapter demonstrates how to use Scala’s object-oriented features to build a module for rendering rectangular text boxes (the authors called them elements). This section describes the underlying idea without the object-oriented fluff.

The primary type in our library is the box: a two-dimensional array of ascii characters Single-byte characters aren’t a fundamental restriction; we could also arrange Unicode glyphs in a grid. . A box has a height (the number of rows) and a width (the number of columns). There are two primary ways to construct a box: of_string wraps a string into a unit-height grid Handling multiline strings is an exercise for the reader. and fill fills a box of specified dimensions with a character. The space and of_char constructors are special cases of fill. An empty box has zero dimensions and acts as a neutral element; combining it with other boxes has no effect.

Primitive box constructors.
                            +-------------+
of_string "Hello, World!" = |Hello, World!|
                            +-------------+

               +----+
               |aaaa|
fill 'a' 3 4 = |aaaa|
               |aaaa|
               +----+

            +--+
            |  |
space 3 2 = |  |
            |  |
            +--+

              +-+
of_char 'a' = |a|
              +-+

empty = ++
        ++

Things get interesting when we start combining the primitives. We can compose two boxes in at least two ways: by stacking them horizontally (placing the first box beside the second) or vertically (placing the first box above the second).

Box combinators beside and above stack boxes horizontally and vertically.
+--+        +--+   +----+
|aa|        |bb|   |aabb|
|aa| beside |bb| = |aabb|
|aa|        |bb|   |aabb|
+--+        +--+   +----+

                    +--+
+--+         +--+   |aa|
|aa|         |bb|   |aa|
|aa|  above  |bb| = |aa|
|aa|         |bb|   |bb|
+--+         +--+   |bb|
                    |bb|
                    +--+

For the composite box to have well-defined height and width, the arguments must have compatible dimensions: vertically stacked boxes must have the same width, and horizontally stacked boxes must have the same height.

We solve this issue by padding the smaller box with extra space: we widen it for vertical composition and heighten it for horizontal composition. We can add the padding before, after, or around the smaller box. Since none of the options is inherently superior, we provide all three, using central alignment as the default.

        +---+   +-------+
widen 7 |aaa| = |  aaa  |
        +---+   +-------+

                 +-+
           +-+   | |
heighten 3 |b| = |b|
           +-+   | |
                 +-+

+---+              +----+
|aaa|        +-+   |aaa |
|aaa| beside |b| = |aaab|
|aaa|        +-+   |aaa |
+---+              +----+

+---+         +-+   +---+
|aaa|  above  |b| = |aaa|
+---+         +-+   | b |
                    +---+

The hconcat (concatenate horizontally) and vconcat (concatenate vertically) stack an array of boxes (beside and above compose exactly two boxes). The grid function takes a 2-d array of boxes, combines each row horizontally, and then combines the rows vertically.

Box combinators operating on arrays of boxes.
           +-+  +-+  +-+      +---+
hconcat [| |a|; |b|; |c| |] = |abc|
           +-+  +-+  +-+      +---+

                              +-+
           +-+  +-+  +-+      |a|
vconcat [| |a|; |b|; |c| |] = |b|
           +-+  +-+  +-+      |c|
                              +-+

           +-+  +-+        +-+  +-+         +--+
grid [| [| |a|; |b| |]; [| |c|; |d| |] |] = |ab|
           +-+  +-+        +-+  +-+         |cd|
                                            +--+

Examples

Sierpinski triangle

Box combinators are a powerful tool for playing with fractals. Rendering a Sierpinski triangle requires only a few lines of code.

A program drawing a Sierpinski triangle of order n.
let rec sierpinski n =
    if n == 0 then Box.of_char '*'
    else let s = sierpinski (n - 1) in
         Box.above s (Box.hconcat [| s; Box.of_char ' '; s |])
A Sierpinski triangle rendered using box combinators.
$ sierpinski 5 |> Box.print_box

                               *
                              * *
                             *   *
                            * * * *
                           *       *
                          * *     * *
                         *   *   *   *
                        * * * * * * * *
                       *               *
                      * *             * *
                     *   *           *   *
                    * * * *         * * * *
                   *       *       *       *
                  * *     * *     * *     * *
                 *   *   *   *   *   *   *   *
                * * * * * * * * * * * * * * * *
               *                               *
              * *                             * *
             *   *                           *   *
            * * * *                         * * * *
           *       *                       *       *
          * *     * *                     * *     * *
         *   *   *   *                   *   *   *   *
        * * * * * * * *                 * * * * * * * *
       *               *               *               *
      * *             * *             * *             * *
     *   *           *   *           *   *           *   *
    * * * *         * * * *         * * * *         * * * *
   *       *       *       *       *       *       *       *
  * *     * *     * *     * *     * *     * *     * *     * *
 *   *   *   *   *   *   *   *   *   *   *   *   *   *   *   *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

Spiral

The following snippet is a modified version of the spiral renderer from section 10.15 of Programming in Scala.

A program drawing a spiral with n turns.
let rec spiral n =
    let open Box in
    if n <= 0
    then of_char '+'
    else let s = spiral (n - 1) in
         let h, w = dimensions s in
         let vbar = fill '|' h 1 in
         grid [| [| of_string "| +"; fill '-' 1 w; of_char '+' |]
               ; [| vbar; of_char ' '; s; of_char ' '; vbar |]
               ; [| of_char '+'; fill '-' 1 (w+2); of_char '+' |] |]
$ spiral 4 |> Box.print_box

| +-------------+
| | +---------+ |
| | | +-----+ | |
| | | | +-+ | | |
| | | | + | | | |
| | | +---+ | | |
| | +-------+ | |
| +-----------+ |
+---------------+

Data table

Playing with fractals is fun, but it won’t pay our bills. Business loves tables, and box combinators are a powerful tool for rendering them. Let’s display an array of book metadata in a human-friendly way.

type book = { title : string;
              author : string;
              rating : int;
              price : float }

let books = [| { title = "Waiting for Good Dough";
                 author = "Samuel Biscuit";
                 rating = 4;
                 price = 23.86 };
               { title = "The Bun Also Rises";
                 author = "Ernest Hemingwaffle";
                 rating = 5;
                 price = 9.86 };
               { title = "Yeast of Eden";
                 author = "John Sconebeck";
                 rating = 2;
                 price = 6.00 };
               { title = "One Hundred Years of Solid Food";
                 author = "G. Gordita Marquez";
                 rating = 4;
                 price = 17.00 }
             |]

The make_table function transforms records into a table in three steps:

  1. Convert each field into a text box and stack related fields vertically, aligning them according to their data type (lines 6–9).
  2. Place a column header above each of the resulting columns (lines 11–15).
  3. Put vertical bars around the titled column boxes (line 17).
The make_table function renders a book metadata array as an ascii table.
let make_table t =
    let open Box in
    let make_column f align =
        Array.map (fun b -> f b |> of_string) t |> vconcat ~align in
    let cols = [|
        ("Title",  make_column (fun b -> b.title) `Left);
        ("Author", make_column (fun b -> b.author) `Left);
        ("Rating", make_column (fun b -> String.make (b.rating) '*') `Left);
        ("Price",  make_column (fun b -> Printf.sprintf "%.2f" b.price) `Right);
    |] in
    let titled = Array.map (fun (h, column) ->
        let header = of_string h in
        let hbar = fill '-' 1 (max (width header) (width column) + 2)
        in vconcat [| header; hbar; column |]
    ) cols in
    let vbar = fill '|' (height titled.(0)) 1 in
    Array.fold_left (fun acc col -> hconcat [| acc; col; vbar |]) vbar titled

Rendering the book metadata results in a neat ascii table.

Rendered book metadata array.
$ make_table books |> Box.print_box

|              Title              |       Author        | Rating | Price |
|---------------------------------|---------------------|--------|-------|
| Waiting for Good Dough          | Samuel Biscuit      | ****   | 23.86 |
| The Bun Also Rises              | Ernest Hemingwaffle | *****  |  9.86 |
| Yeast of Eden                   | John Sconebeck      | **     |  6.00 |
| One Hundred Years of Solid Food | G. Gordita Marquez  | ****   | 17.00 |

Closing words

If you found box combinators interesting and want to play with them:

Appendix: the Box module

This text box implementation is not the most efficient since combining boxes in a loop has quadratic complexity. A more sophisticated design would combine boxes lazily, delaying concatenations until the last moment or avoiding them entirely. However, the simple approach is good enough for data that fits on a screen.

A simple implementation of the box combinator library. This code belongs in the box.ml file.
type t = string array

let height b = Array.length b

let width b = if Array.length b == 0 then 0 else String.length b.(0)

(** Returns the box height and width. *)
let dimensions b = height b, width b

(** Prints box b to the standard output. *)
let print_box b = Array.iter print_endline b

(** Creates a box large enough to hold string s. *)
let of_string s = [| s |]

(** Creates an h×w box filled with character c. *)
let fill c h w = Array.make h (String.make w c)

(** Creates a 1×1 box containing character c. *)
let of_char c = fill c 1 1

(** Creates an h×w box filled with spaces. *)
let space h w = fill ' ' h w

(** An empty box. *)
let empty = space 0 0

(** The vertical alignment type. *)
type vertical   = [ `Top  | `Center | `Bottom ]

(** The horizontal alignment type. *)
type horizontal = [ `Left | `Center | `Right  ]

(** Stack box l to the left of box r. *)
let rec beside ?(align:vertical = `Center) l r =
    if width l == 0 then r else if width r == 0 then l
    else let hl = heighten ~align (height r) l in
         let hr = heighten ~align (height l) r in
         Array.map2 String.cat hl hr

(** Stack box t above of box b. *)
and above ?(align:horizontal = `Center) t b =
    if height t == 0 then b else if height b == 0 then t
    else let wt = widen ~align (width b) t in
         let wb = widen ~align (width t) b in
         Array.append wt wb

(** Makes box b at least w units wide. *)
and widen ?(align:horizontal = `Center) w b =
    if width b >= w then b
    else let bh, bw = height b, width b in
         let pw = w - bw in
         match align with
         | `Left   -> beside b (space bh pw)
         | `Right  -> beside (space bh pw) b
         | `Center -> hconcat
                      [| space bh (pw/2); b; space bh (pw - pw/2) |]

(** Makes box b at least h units high. *)
and heighten ?(align:vertical = `Center) b h =
    if height b >= h then b
    else let bh, bw = height b, width b in
         let ph = h - bh in
         match align with
         | `Top    -> above b (space ph bw)
         | `Bottom -> above (space ph bw) b
         | `Center -> vconcat [| space (ph/2) bw
                               ; b
                               ; space (ph - ph/2) bw |]

(** Stacks an array of boxes horizontally. *)
and hconcat ?(align:vertical = `Center) boxes =
    Array.fold_left (beside ~align) empty boxes

(** Stacks an array of boxes vertically. *)
and vconcat ?(align:horizontal = `Center) boxes =
    Array.fold_left (above ~align) empty boxes

(** Arranges a 2-D array of boxes. *)
let grid g = Array.map hconcat g |> vconcat

(** Draws an ASCII art frame around box b. *)
let framed b =
    let h, w = dimensions b in
    let vbar = fill '|' h 1 in
    let hbar = fill '-' 1 w in
    let corner = of_char '+' in
    grid [| [| corner; hbar; corner |]
          ; [| vbar;   b;    vbar   |]
          ; [| corner; hbar; corner |] |]

Similar articles