Box combinators
✏ 2025-06-01 ✂ 2025-06-01A 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.
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).beside
and above
stack boxes horizontally and vertically.
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.
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.
Examples
Sierpinski triangle
Box combinators are a powerful tool for playing with fractals.
Rendering a Sierpinski triangle requires only a few lines of code.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 |])
$ 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.
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:
- Convert each field into a text box and stack related fields vertically, aligning them according to their data type (lines 6–9).
- Place a column header above each of the resulting columns (lines 11–15).
- Put vertical bars around the titled column boxes (line 17).
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.$ 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:
- Implement them in your preferred language.
- Use them to visualize complex data. If you have no good ideas, formatting a calendar will stretch your box-welding skills.
-
Study their graphical counterparts that appear in both
Structure and Interpretation of Computer Programs
(section 2.2.4) andAlgebra-Driven Design
by Sandy Maguire (chapter 2).
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.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
- ONNX introduction
- Effective design docs
- Static types are for perfectionists
- Transposing tensor files
- The plan-execute pattern