Ropes: An Alternative to Strings
| (require rope) | package: rope |
A rope is a tree of small string (or byte, or vector, or other) chunks. It supports the same operations as a string, but where a string of length n takes O(n) time to split, splice, or concatenate, a balanced rope does the same work in O(log n). This library provides the rope data structure, a generic protocol for building ropes out of any chunkable data, and ready-made instances for strings and byte strings.
Based on the paper by Boehm, Atkinson, and Plass.
1 Introduction
Racket strings are immutable, fixed-length vectors of characters. That makes them cheap to read and share, but expensive to edit. Inserting a character in the middle of a string of length n means allocating and copying a new string of length n, because substring and string-append both have to walk the whole thing. A text editor that repeatedly inserts and deletes characters in a large buffer this way pays O(n) per keystroke, and O(n2) (or worse) over the course of editing the whole document.
Ropes solve this by representing a long sequence as a balanced binary tree of short chunks rather than as a flat array. Each leaf holds a chunk small enough to slice or copy cheaply. Each internal node caches the combined length of its subtrees, so that length queries are O(1) and a split or concatenation only has to touch O(log n) nodes along a single root-to-leaf path. Hence, splicing new content into a rope costs O(log n) to find the split points plus the cost of building a subtree for the inserted text, rather than O(n) to rebuild the whole sequence.
Asymptotic complexity is only half the story—
This library separates the tree structure from what it holds. The rope
type itself only knows about leaves and nodes—
2 Quick Start
The most common way to use this library is through rope/string, which treats a rope as a persistent, efficiently editable stand-in for a string. You can build one with string->rope and get an ordinary string back with rope->string:
> (define greeting (string->rope "Hello!")) > (rope-length greeting) 6
> (rope->string greeting) "Hello!"
Editing a rope never touches its argument; every operation returns a new rope that shares structure with the old one. string-rope-splice replaces a run of characters with a new string, and inserting text is just the special case where nothing is replaced:
> (define with-name (string-rope-splice greeting 5 0 " World")) > (rope->string with-name) "Hello World!"
; the original is untouched > (rope->string greeting) "Hello!"
Deleting text is just splicing in the empty string.
> (define bare (string-rope-splice with-name 5 7 "")) > (rope->string bare) "Hello"
Building up a large document out of pieces is a matter of appending ropes, which is also O(log n):
> (define doc (string-rope-append (string->rope "It was the best of times, ") (string->rope "it was the worst of times."))) > (rope->string doc) "It was the best of times, it was the worst of times."
Ropes are also sequences of their elements, so a rope of characters works with the various for forms via in-string-rope:
> (for/list ([ch (in-string-rope doc)] #:when (char-upper-case? ch)) ch) '(#\I)
Everything above works identically for byte strings with rope/bytes in place of rope/string.
3 Concepts
3.1 Leaves, Nodes, and Balance
A rope is either a rope-leaf, holding one chunk of raw data directly, or a rope-node, holding a left and a right sub-rope. Both variants cache their element count and width (see below), which is what makes rope-length an O(1) operation instead of a tree walk.
Without careful management, repeated splitting and appending can produce a tree that degenerates into a linked list, at which point operations that ought to be O(log n) would become O(n) again. Following Boehm, Atkinson, and Plass, this library keeps every rope balanced by maintaining the invariant that a rope of depth d always has at least Fib(d + 2) leaves, where Fib is the Fibonacci sequence. rope-append1 checks this invariant after every naive concatenation and, when it is violated, rebuilds the offending subtree from scratch in O(n) time. Because a rebuild is only triggered when the tree has drifted measurably out of balance, this happens at most O(log n) times over the course of O(n) edits, making rope-append1 amortized O(log n).
3.2 Count and Width
Every rope tracks two numbers: its count, the number of elements it
holds, and its width, the total extent of those elements along some
other axis. For the string and byte-string ropes in this library, every element
occupies exactly one unit of width—
The two numbers diverge for a chunk type whose elements have variable extent. For example, in a rope over a sequence of on-screen glyphs, each glyph is a single element that may occupy a variable number of display columns. rope-offset-index exists for exactly this situation: given a rope and a width, such as a column number, it locates the count-based element index at that position in O(log n) time. For the built-in types this is a straightforward binary search, but the machinery is there for chunk types that need it.
3.3 Cursors
A cursor is a position within a rope, represented as the raw chunk currently being read, an offset within that chunk, and a rope of everything after it. Cursors support O(1) cursor-peek and cursor-advance except when crossing from one leaf into the next, which is itself amortized O(1) over the length of a leaf. cursor-drop skips k elements, and cursor-take extracts the next k elements as a rope. Both run in O(log n) time, independent of k, by splitting the rope rather than visiting each element.
Cursors are the mechanism behind rope-foldl and rope-foldr, which fold over one or more ropes in lockstep, and behind sequence constructors such as in-string-rope.
3.4 The gen:ropeable Interface
Everything above is implemented once, generically, in terms of a small interface, gen:ropeable, that describes how to work with a raw chunk. A chunk type implements gen:ropeable by supplying:
a predicate identifying values of that chunk type (raw?);
a maximum chunk size for a leaf (raw-limit) and a way to construct an empty chunk (raw-empty);
ways to slice, append, and index into a chunk (raw-slice, raw-append, raw-ref); and
constructors for type-specific leaf and node structs (rope-leaf-ctor, rope-node-ctor).
Every other operation in Generic Rope Operations—
3.5 Content-Based Equality and Hashing
Every rope produced by define-rope-type implements
gen:equal+hash—
Internally, this is a composable polynomial (Rabin–Karp style) rolling hash. Each node caches ⟨hash, baseⁿ⟩ for its subtree in a weak, eq?-keyed table, so combining two already-hashed subtrees into a parent is O(1). Hashing a freshly built rope of n elements is O(n); hashing it again, or hashing any rope sharing structure with one already hashed, is O(1) amortized. equal? itself takes an eq? fast path; on mismatch, it falls back to an O(n) cursor-based walk that never requires the two ropes’ leaf boundaries to align.
> (define a (string->rope "hello world")) > (define b (string-rope-append (string->rope "hello ") (string->rope "world"))) > (equal? a b) #t
> (equal? (equal-hash-code a) (equal-hash-code b)) #t
4 Generic Rope Operations
| (require rope/rope) | package: rope |
This module defines the rope tree structure, the cursor type, and the gen:ropeable interface that connects the two to a particular chunk representation. Most programs will use a type-specific module such as rope/string instead of calling these generic operations directly; this reference is here for readers implementing a new chunk type, and to document what the type-specific operations reduce to underneath.
4.1 Ropes
struct
count : exact-nonnegative-integer? width : exact-nonnegative-integer? raw : any/c
struct
count : exact-nonnegative-integer? width : exact-nonnegative-integer? left : rope? right : rope?
procedure
(rope-count rope) → exact-nonnegative-integer?
rope : rope?
procedure
(rope-width rope) → exact-nonnegative-integer?
rope : rope?
procedure
(rope-length rope) → exact-nonnegative-integer?
rope : rope?
procedure
(rope-depth rope) → exact-nonnegative-integer?
rope : rope?
procedure
(rope-empty? rope) → boolean?
rope : rope?
> (rope-empty? (string->rope "")) #t
> (rope-empty? (string->rope "x")) #f
procedure
(rope-balanced? rope) → boolean?
rope : rope?
procedure
(rope-flatten rope) → list?
rope : rope?
4.2 The gen:ropeable Interface
value
procedure
(rope-leaf-ctor ropeable) → procedure?
ropeable : ropeable?
procedure
(rope-node-ctor ropeable) → procedure?
ropeable : ropeable?
4.2.1 Raw Chunk Operations
These describe the raw, non-rope representation that a gen:ropeable instance is built around.
procedure
(raw-limit ropeable) → exact-nonnegative-integer?
ropeable : ropeable?
procedure
(raw-count ropeable raw) → exact-nonnegative-integer?
ropeable : ropeable? raw : any/c
procedure
(raw-width ropeable raw) → exact-nonnegative-integer?
ropeable : ropeable? raw : any/c
procedure
ropeable : ropeable? raw : any/c start : exact-nonnegative-integer? end : exact-nonnegative-integer?
procedure
(raw-append ropeable raw ...) → any/c
ropeable : ropeable? raw : any/c
procedure
ropeable : ropeable? raw : any/c pos : exact-nonnegative-integer?
4.2.2 Rope Construction and Editing
These are the same operations exposed under type-specific names by rope/string and rope/bytes; see those sections for usage examples. Each takes an explicit gen:ropeable instance identifying which chunk type’s rules to follow.
procedure
(make-rope-leaf ropeable raw) → rope-leaf?
ropeable : ropeable? raw : any/c
procedure
(make-rope-node ropeable left right) → rope-node?
ropeable : ropeable? left : rope? right : rope?
procedure
(make-empty-rope ropeable) → rope?
ropeable : ropeable?
procedure
(rope-concat ropeable left right) → rope?
ropeable : ropeable? left : rope? right : rope?
procedure
(rope-append1 ropeable left right) → rope?
ropeable : ropeable? left : rope? right : rope?
procedure
(rope-append ropeable rope ...) → rope?
ropeable : ropeable? rope : rope?
procedure
(rope-split ropeable rope i) →
rope? rope? ropeable : ropeable? rope : rope? i : exact-nonnegative-integer?
procedure
(rope-offset-index ropeable rope ofs)
→ exact-nonnegative-integer? ropeable : ropeable? rope : rope? ofs : exact-nonnegative-integer?
procedure
(rope-splice ropeable rope start old-len new-raw) → rope? ropeable : ropeable? rope : rope? start : exact-nonnegative-integer? old-len : exact-nonnegative-integer? new-raw : any/c
procedure
(rope-slice ropeable rope start len) → rope?
ropeable : ropeable? rope : rope? start : exact-nonnegative-integer? len : exact-nonnegative-integer?
procedure
(raw-compare ropeable raw1 raw2) → (or/c '< '= '>)
ropeable : ropeable? raw1 : any/c raw2 : any/c
procedure
(rope-compare-with ropeable cmp-proc rope1 rope2) → (or/c '< '= '>) ropeable : ropeable? cmp-proc : procedure? rope1 : rope? rope2 : rope?
procedure
(rope-compare ropeable rope1 rope2) → (or/c '< '= '>)
ropeable : ropeable? rope1 : rope? rope2 : rope?
Note this is not the same check as equal? on two ropes: this comparator-based check requires the instantiating type to have supplied #:compare to define-rope-type, and does not benefit from the memoized content-hash caching described in Content-Based Equality and Hashing. Prefer equal? for a plain equality test; use rope=? when you already need rope-compare’s ordering (e.g. alongside rope<?) and want the equal-case answer from the same family of operations.
4.3 Cursors
struct
raw : any/c pos : exact-nonnegative-integer? after : rope?
procedure
(cursor-at-end? ropeable cursor) → boolean?
ropeable : ropeable? cursor : cursor?
procedure
(cursor-peek ropeable cursor) → any/c
ropeable : ropeable? cursor : cursor?
procedure
(cursor-advance ropeable cursor) → cursor?
ropeable : ropeable? cursor : cursor?
procedure
(cursor-drop ropeable cursor k) → cursor?
ropeable : ropeable? cursor : cursor? k : exact-nonnegative-integer?
procedure
(cursor-take ropeable cursor k) → rope?
ropeable : ropeable? cursor : cursor? k : exact-nonnegative-integer?
procedure
(rope->cursor ropeable rope) → cursor?
ropeable : ropeable? rope : rope?
procedure
(cursor->rope ropeable cursor) → rope?
ropeable : ropeable? cursor : cursor?
4.4 Cursor-Based Rope Operations
procedure
(rope-foldl ropeable proc init rope ...+) → any/c
ropeable : ropeable? proc : procedure? init : any/c rope : rope?
procedure
(rope-foldr ropeable proc init rope ...+) → any/c
ropeable : ropeable? proc : procedure? init : any/c rope : rope?
5 String Ropes
| (require rope/string) | package: rope |
A rope specialized for string? chunks, generated by define-rope-type (Defining New Rope Types). Every operation below behaves exactly like its generic counterpart in rope/rope, specialized to strings and with no need to supply a ropeable instance explicitly.
> (define r (string->rope "supercalifragilisticexpialidocious")) > (rope-length r) 34
> (rope-depth r) 0
> (rope->string (string-rope-slice r 5 4)) "cali"
struct
(struct string-rope-leaf rope-leaf (count width raw))
count : exact-nonnegative-integer? width : exact-nonnegative-integer? raw : string?
struct
(struct string-rope-node rope-node (count width left right))
count : exact-nonnegative-integer? width : exact-nonnegative-integer? left : string-rope? right : string-rope?
procedure
(string-rope? v) → boolean?
v : any/c
procedure
(string-raw? v) → boolean?
v : any/c
procedure
procedure
procedure
raw : string?
procedure
raw : string?
procedure
(string-raw-slice raw start end) → string?
raw : string? start : exact-nonnegative-integer? end : exact-nonnegative-integer?
procedure
(string-raw-ref raw pos) → char?
raw : string? pos : exact-nonnegative-integer?
procedure
(string-raw-append raw ...) → string?
raw : string?
procedure
raw : string?
value
procedure
(string->rope str) → string-rope?
str : string?
> (string->rope "a balanced tree of text") (string-rope-leaf 23 23 "a balanced tree of text")
procedure
(rope->string rope) → string?
rope : string-rope?
procedure
(string-rope-append1 left right) → string-rope?
left : string-rope? right : string-rope?
procedure
(string-rope-append rope ...) → string-rope?
rope : string-rope?
> (rope->string (string-rope-append (string->rope "one ") (string->rope "two ") (string->rope "three"))) "one two three"
procedure
(string-rope-split rope i) →
string-rope? string-rope? rope : string-rope? i : exact-nonnegative-integer?
procedure
(string-rope-offset-index rope ofs) → exact-nonnegative-integer?
rope : string-rope? ofs : exact-nonnegative-integer?
procedure
(string-rope-splice rope start old-len new-str) → string-rope? rope : string-rope? start : exact-nonnegative-integer? old-len : exact-nonnegative-integer? new-str : string?
> (define r (string->rope "a tree of text")) > (rope->string (string-rope-splice r 2 4 "rope")) "a rope of text"
procedure
(string-rope-slice rope start len) → string-rope?
rope : string-rope? start : exact-nonnegative-integer? len : exact-nonnegative-integer?
procedure
(string-rope-compare rope1 rope2) → (or/c '< '= '>)
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope=? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope<? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope<=? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope>? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope>=? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope-ci-compare rope1 rope2) → (or/c '< '= '>)
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope-ci<? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope-ci<=? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope-ci>? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-rope-ci>=? rope1 rope2) → boolean?
rope1 : string-rope? rope2 : string-rope?
procedure
(string-cursor-at-end? cursor) → boolean?
cursor : cursor?
procedure
(string-cursor-peek cursor) → (or/c #f char?)
cursor : cursor?
procedure
(string-cursor-advance cursor) → cursor?
cursor : cursor?
procedure
(string-cursor-drop cursor k) → cursor?
cursor : cursor? k : exact-nonnegative-integer?
procedure
(string-cursor-take cursor k) → string-rope?
cursor : cursor? k : exact-nonnegative-integer?
procedure
(string-rope->cursor rope) → cursor?
rope : string-rope?
procedure
(cursor->string-rope cursor) → string-rope?
cursor : cursor?
procedure
(string-rope-foldl proc init rope ...+) → any/c
proc : procedure? init : any/c rope : string-rope?
> (string-rope-foldl (λ (acc ch) (cons ch acc)) '() (string->rope "abc")) '(#\c #\b #\a)
procedure
(string-rope-foldr proc init rope ...+) → any/c
proc : procedure? init : any/c rope : string-rope?
procedure
(in-string-rope rope) → sequence?
rope : string-rope?
> (for/list ([ch (in-string-rope (string->rope "rope"))]) ch) '(#\r #\o #\p #\e)
procedure
(open-input-string-rope rope) → input-port?
rope : string-rope?
> (define in (open-input-string-rope (string->rope "line one\nline two"))) > (read-line in) "line one"
6 Byte Ropes
| (require rope/bytes) | package: rope |
A rope specialized for bytes? chunks, generated by define-rope-type in exactly the same way as rope/string. Every operation below is the byte-string version of the corresponding string-rope operation described in the previous section.
struct
(struct bytes-rope-leaf rope-leaf (count width raw))
count : exact-nonnegative-integer? width : exact-nonnegative-integer? raw : bytes?
struct
(struct bytes-rope-node rope-node (count width left right))
count : exact-nonnegative-integer? width : exact-nonnegative-integer? left : bytes-rope? right : bytes-rope?
procedure
(bytes-rope? v) → boolean?
v : any/c
procedure
(bytes-raw? v) → boolean?
v : any/c
procedure
procedure
procedure
raw : bytes?
procedure
raw : bytes?
procedure
(bytes-raw-slice raw start end) → bytes?
raw : bytes? start : exact-nonnegative-integer? end : exact-nonnegative-integer?
procedure
(bytes-raw-ref raw pos) → byte?
raw : bytes? pos : exact-nonnegative-integer?
procedure
(bytes-raw-append raw ...) → bytes?
raw : bytes?
procedure
(make-bytes-rope-leaf raw) → bytes-rope-leaf?
raw : bytes?
value
procedure
(bytes->rope bstr) → bytes-rope?
bstr : bytes?
procedure
(rope->bytes rope) → bytes?
rope : bytes-rope?
procedure
(bytes-rope-append1 left right) → bytes-rope?
left : bytes-rope? right : bytes-rope?
procedure
(bytes-rope-append rope ...) → bytes-rope?
rope : bytes-rope?
procedure
(bytes-rope-split rope i) →
bytes-rope? bytes-rope? rope : bytes-rope? i : exact-nonnegative-integer?
procedure
(bytes-rope-offset-index rope ofs) → exact-nonnegative-integer?
rope : bytes-rope? ofs : exact-nonnegative-integer?
procedure
(bytes-rope-splice rope start old-len new-bstr) → bytes-rope? rope : bytes-rope? start : exact-nonnegative-integer? old-len : exact-nonnegative-integer? new-bstr : bytes?
> (define r (bytes->rope #"hello world")) > (rope->bytes (bytes-rope-splice r 6 5 #"racket")) #"hello racket"
procedure
(bytes-rope-slice rope start len) → bytes-rope?
rope : bytes-rope? start : exact-nonnegative-integer? len : exact-nonnegative-integer?
procedure
(bytes-rope-compare rope1 rope2) → (or/c '< '= '>)
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-rope=? rope1 rope2) → boolean?
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-rope<? rope1 rope2) → boolean?
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-rope<=? rope1 rope2) → boolean?
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-rope>? rope1 rope2) → boolean?
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-rope>=? rope1 rope2) → boolean?
rope1 : bytes-rope? rope2 : bytes-rope?
procedure
(bytes-cursor-at-end? cursor) → boolean?
cursor : cursor?
procedure
(bytes-cursor-peek cursor) → (or/c #f byte?)
cursor : cursor?
procedure
(bytes-cursor-advance cursor) → cursor?
cursor : cursor?
procedure
(bytes-cursor-drop cursor k) → cursor?
cursor : cursor? k : exact-nonnegative-integer?
procedure
(bytes-cursor-take cursor k) → bytes-rope?
cursor : cursor? k : exact-nonnegative-integer?
procedure
(bytes-rope->cursor rope) → cursor?
rope : bytes-rope?
procedure
(cursor->bytes-rope cursor) → bytes-rope?
cursor : cursor?
procedure
(bytes-rope-foldl proc init rope ...+) → any/c
proc : procedure? init : any/c rope : bytes-rope?
procedure
(bytes-rope-foldr proc init rope ...+) → any/c
proc : procedure? init : any/c rope : bytes-rope?
procedure
(in-bytes-rope rope) → sequence?
rope : bytes-rope?
procedure
(open-input-bytes-rope rope) → input-port?
rope : bytes-rope?
7 Defining New Rope Types
| (require rope/define-rope-type) | package: rope |
syntax
(define-rope-type type raw? raw-limit raw-empty raw-count raw-width raw-slice raw-append raw-ref)
raw? : (any/c . -> . boolean?)
raw-limit : (-> exact-nonnegative-integer?)
raw-empty : (-> any/c)
raw-count : (any/c . -> . exact-nonnegative-integer?)
raw-width : (any/c . -> . exact-nonnegative-integer?)
raw-slice :
(any/c exact-nonnegative-integer? exact-nonnegative-integer? . -> . any/c)
raw-append : (list? . -> . any/c)
raw-ref : (any/c exact-nonnegative-integer? . -> . any/c)
raw? is a one-argument predicate identifying valid chunks;
raw-limit is a zero-argument procedure returning the maximum count of a single leaf;
raw-empty is a zero-argument procedure constructing an empty chunk;
raw-count and raw-width are one-argument procedures returning a chunk’s count and width;
raw-slice is a three-argument procedure, taking a chunk followed by start and end indices, and returning a sub-chunk;
raw-append is a one-argument procedure that takes a list of chunks and returns their concatenation; and
raw-ref is a two-argument procedure that takes a chunk and an index and returns a single element.
Given these, define-rope-type defines, among others, the following bindings, where type stands for the identifier bound to type:
structs type-rope-leaf and type-rope-node, both subtypes of rope, and a predicate type-rope?;
type-raw?, type-raw-limit, type-raw-empty, type-raw-count, type-raw-width, type-raw-slice, type-raw-append, and type-raw-ref, each specialized versions of the procedure supplied above;
make-_type-rope-leaf, make-empty-_type-rope, type-rope-append1, type-rope-append, type-rope-split, type-rope-offset-index, type-rope-splice, type-rope-slice, type->-rope, and rope->_type, the specialized construction and editing operations from Generic Rope Operations;
type-rope-compare, type-rope-compare-with, type-rope=?, type-rope<?, type-rope<=?, type-rope>?, and type-rope>=?, the specialized comparison operations (raises at call time unless #:compare was supplied to define-rope-type);
type-cursor-at-end?, type-cursor-peek, type-cursor-advance, type-cursor-drop, type-cursor-take, type-rope->cursor, and cursor->_type-rope, the specialized cursor operations; and
type-rope-foldl, type-rope-foldr, and in-_type-rope, the specialized fold and sequence operations.
define-rope-type does not provide any of these bindings; it only defines them in the enclosing module. A module that uses define-rope-type is expected to re-export the bindings it wants to make public, typically via rope-type-out or rope-type-out/contract (below).
syntax
(rope-type-out type)
Use this inside a trusted module where paying for contract checks at every rope operation isn’t worthwhile.
syntax
(rope-type-out/contract type maybe-raw maybe-element)
maybe-raw =
| #:raw raw-contract-expr maybe-element =
| #:element element-contract-expr
raw-contract-expr : contract?
element-contract-expr : contract?
in-_type-rope is re-exported bare, since a sequence macro has no useful contract. Its runtime fallback in-_type-rope-runtime, which is used when the sequence is handed to a higher-order function like sequence-map instead of appearing directly in a for clause, is contracted and re-exported alongside it.
Note that type-cursor-peek’s contract always admits #f in addition to element-contract-expr, since #f signals that the cursor has run out of elements rather than being itself an element value.
As a complete example, here is a rope type over immutable vectors, exposing only the operations needed to build, edit, and read one back:
#lang racket/base (require racket/contract rope/define-rope-type rope/rope) (provide (contract-out [vector-rope? (any/c . -> . boolean?)] [vector->rope (vector? . -> . vector-rope?)] [rope->vector (vector-rope? . -> . vector?)] [vector-rope-append (vector-rope? ... . -> . vector-rope?)] [vector-rope-splice (vector-rope? exact-nonnegative-integer? exact-nonnegative-integer? vector? . -> . vector-rope?)] [vector-rope-slice (vector-rope? exact-nonnegative-integer? exact-nonnegative-integer? . -> . vector-rope?)])) (define-rope-type vector vector? (λ () 256) (λ () (vector)) vector-length vector-length (λ (v start end) (vector-copy v start end)) (λ (vs) (apply vector-append vs)) vector-ref)
Every operation on the resulting vector-rope type (appending, splicing, slicing, folding, iterating with in-vector-rope) behaves exactly like its string and byte-string counterparts, at the same complexity, without any further code.
The provide clause above spells out each binding’s contract by hand, which is worth doing for a small, curated surface like this one. A type that wants to expose everything define-rope-type generates can skip the boilerplate:
(provide (rope-type-out/contract vector #:raw vector?))
8 Performance Summary
The following table summarizes the running time of the principal operations, for a rope of n elements. Amortized entries can occasionally cost more on a single call, when a rebalancing rebuild is triggered, but never more than O(log n) on average over a sequence of edits.
Operation | Time |
O(1) | |
O(1) | |
O(1) | |
O(log n) amortized | |
O(log n) amortized | |
O(log n + m) amortized | |
O(log n + k) amortized | |
rope-compare/rope<?/etc. | O(log n + d) amortized |
O(log n) | |
O(1) amortized | |
O(log n) | |
O(log n) | |
O(n) | |
O(n) | |
O(# leaves) | |
O(n) worst case, O(1) if eq? or already hashed | |
O(n) cold, O(1) amortized (cached) |
Here m is the count of the chunk being spliced in, k is the number of leaves spanned by an extracted slice, and d is the position of the first difference.
9 Measured Performance
The numbers below come from this package’s own benchmark suite (documented in benchmark/README.md), run against a freshly built package so results reflect compiled (not interpreted) code.
Test machine: benchmarks were performed on a desktop PC with a 12th Gen Intel(R) Core(TM) i7-12700K with 12 cores (8 P-cores, 4 E-cores) and 20 threads, 32GiB DDR5 DRAM at 4800 MT/s, on Arch linux (July 2026) with Racket v9.2 [cs].
Methodology: default suite settings unless noted—
racco setup rope |
racket benchmark/main.rkt --save /tmp/run.json |
racket benchmark/visualize.rkt --in /tmp/run.json --order scenario |
Here are some of the more impactful figures from that chart, at the largest test size (32,768 elements):
Operation | Time at n = 32,768 |
append1 (leaf-level) | ~110-120ns |
split at midpoint | ~3us |
slice, one quarter of the rope | ~5-6us |
splice 4 elements at the midpoint | ~9us |
offset-index at the midpoint | ~10-11us |
build from a flat raw chunk | ~30-170us |
equal-hash-code, already hashed (warm) | <60ns, flat across all sizes |
equal-hash-code, never hashed before (cold) | ~5.8-6.0ms |
equal?, same content (post-optimization, see below) | ~200-550us |
The *-build rows in the full chart (typed-build, fragmented-build, edited-build) are the slowest operations measured, at tens to hundreds of milliseconds. This is expected, since they simulate building a rope by thousands of individual edits, which dominates the elapsed real time cost of running the full suite. Everything else in the table is a single operation on an already-built rope.
9.1 Case study: optimizing equal?
As a concrete example of how the benchmarking suite is used, here are the results of a real optimization made during development. The original content-equality check compared two ropes element by element, and it was changed to compare whole overlapping chunks at once. This was the same strategy rope-compare already used, and we hypothesized that one equal? call per chunk would beat many individual per-character calls.
The result was 74-98% reductions (roughly 8-13x) across every scenario
that requires walking real content, with no measurable effect on
unrelated operations. We also observed a regression—