Racket’s data structures have distinct characteristics that often recommend one over another for a particular job: + Chart adapted from Jens Axel Søgaard.
Structure | Access | Values | Index | Mutable? |
---|---|---|---|---|
pair | random | two | nothing | no |
list | sequential | variable | nothing | no |
hash table | random | variable | keys | optional |
association list | sequential | variable | car | no |
vector | random | fixed | integers | optional |
structure type | random | fixed | identifiers | optional |
Hash tables, vectors, and structure types are available in mutable (meaning, internal values can be changed) and immutable variants. As a rule, unless you need a mutable structure, choose an immutable one, because Racket can better optimize immutable structures for speed.
A pair holds two values. A pair is made with cons, and the values can be retrieved with car and cdr. If you’ve got exactly two values, great. If you’ve got more, look elsewhere.
Pairs explainer
A list holds any number of values. Because lists are made from a sequence of linked pairs, they’re optimized for sequential access, and thus are the characteristic data structures used in iteration and recursion. Random access (with list-ref) is feasible with short lists. For long lists, random access can be slow, so a vector is the better option.
Lists explainer
A vector is an array of values, indexed by location and optimized for random access. Though vectors come in immutable and mutable variants, the length of a vector is always fixed. + But see growable vectors, which can be resized.
A basic mutable vector can be made with vector. Values are retrieved with vector-ref, and changed with vector-set!:
1 2 3 4 5 6 7 8 9 10 | (define vec (vector 'sym "str" 42)) vec ; '#(sym "str" 42) (vector-ref vec 0) ; 'sym (vector-ref vec 1) ; "str" (vector-ref vec 2) ; 42 (vector-set! vec 0 'foo) (vector-set! vec 1 "bar") vec ; '#(foo "bar" 42) (vector-set! vec 1 25) vec ; '#(foo 25 42) |
Vectors and lists can be mutually converted with vector->list and list->vector.
A hash table is a mapping of keys to values, which can themselves be any data type. Hash values can be retrieved with hash-ref:
A hash table can be mutable—meaning, values can be added, deleted, or updated—or immutable. An immutable hash table is created with hash (as demonstrated above). A mutable hash table is created with make-hash, and then can be changed with hash-set!:
By default, a hash table uses equal? to compare keys. Hash tables that use the faster equality functions (eq? and eqv?) are also available. If your keys are only symbols, you can use hasheq or make-hasheq; if your keys are only symbols or numbers, you can use hasheqv or make-hasheqv.
A vector is faster than a hash table, so if you can use sequential integers for keys, a vector is the better choice.
Equality explainer
Hash tables in the Racket Guide
Hash tables in the Racket Reference
The humble association list is often overlooked for small jobs. It’s not a distinct data structure, but rather a list where every element is a pair that holds a key and value (aka an association):
Association lists are easy to make and break, with the help of map:
As lists, association lists work with the usual list functions. They can also be used with assoc (and its variants assv, assq, and assf) to find particular element pairs:
Association lists also can be used in a hash-table-ish way with generic dictionary functions like dict-ref:
Lists explainer
Dictionaries in the Racket Reference
A structure type is a user-defined data structure with an arbitrary number of fields. Like a hash table, a structure type allows access to its fields by name. Like a vector, the number of fields is fixed by the structure-type definition.
The struct form defines the structure type itself, and also manufactures a predicate, plus getters (and, for mutable structure types, setters) for each field. The #:transparent option makes the structure type printable in the REPL:
It’s not uncommon for a Racketeer to throw some values into a list as a quick-and-dirty data structure. But for anything that will be used more than once, a structure type is better:
The getters and setters have readable names.
The structure type can have fields added to its definition without changing existing references.
An instance of a structure type is represented as a single object in memory, so it can be passed around a program “by reference” (meaning, it’s not duplicated if passed from one function to another).
A structure type has its own unique predicate, which can be efficiently tested (say, inside a contract).
The disadvantage of a structure type is that it’s a distinct data type from a hash table, vector, or list, so it can’t be directly used with library functions that consume those types. There’s a little more housekeeping to get values in & out.
Programmer-Defined Datatypes in the Racket Guide
Structure Types in the Racket Reference
Classes—in the object-oriented programming (OOP) sense—are a big deal in many languages. But not in Racket.
Racket has them, of course. They’re used in a couple parts of the library (most prominently, the GUI framework). Otherwise, rarely. In DSLs, almost never.
Why is that? OOP encourages programmers to think about functions as things that are tucked inside data structures as “methods”. Classes themselves often rely on state and mutation.
Whereas with functional programming generally, and Racket in particular, we’re encouraged to think about data structures as things that are consumed by functions. And of course, to avoid state and mutation where possible. So it’s an inversion of priority.
Thus, as a consequence of the functional style, most Racketeers don’t find classes necessary. But they aren’t avoiding OOP as a political gesture. If you want to rock it OOP-style in Racket, go ahead. No one will ever say you’re wrong.
Classes and Objects in the Racket Guide
Classes and Objects in the Racket Reference