txexpr: Tagged X-expressions
(require txexpr) | package: txexpr |
(require (submod txexpr safe)) |
A set of small but handy functions for improving the readability and reliability of programs that operate on tagged X-expressions (for short, txexprs).
1 Installation
raco pkg install txexpr |
raco pkg update txexpr |
2 Importing the module
The module operates in two modes: fast and safe. Fast mode is the default, which you get by importing the module in the usual way: (require txexpr).
Safe mode enables the function contracts documented below. Use safe mode by importing the module as (require (submod txexpr safe)).
3 What’s a txexpr?
It’s an X-expression with the following grammar:
txexpr | = | (list tag (list attr ...) element ...) | ||
| | (cons tag (list element ...)) | |||
tag | = | symbol? | ||
attr | = | (list key value) | ||
key | = | symbol? | ||
value | = | string? | ||
element | = | xexpr? |
A txexpr is a list with a symbol in the first position — the tag — followed by a series of elements, which are other X-expressions. Optionally, a txexpr can have a list of attributes in the second position.
Examples: | ||||||||||||||
|
The last one is a common mistake. Because the key–value pair is not enclosed in a list, it’s interpreted as a nested txexpr within the first txexpr, as you may not find out until you try to read its attributes:
There’s no way of eliminating this ambiguity, short of always requiring an attribute list — empty if necessary — in your txexpr. See also xexpr-drop-empty-attributes.
Examples: | ||||
|
Tagged X-expressions are most commonly found in HTML & XML documents. Though the notation is different in Racket, the data structure is identical:
Examples: | ||||
|
After converting to and from HTML, we get back the original X-expression. Well, almost. The brackets turned into parentheses — no big deal, since they mean the same thing in Racket. Also, per its usual practice, string->xexpr added an empty attribute list after em. This is also benign.
4 Why not just use match, quasiquote, and so on?
If you prefer those, please do. But I’ve found two benefits to using module functions:
Readability. In code that already has a lot of matching and quasiquoting going on, these functions make it easy to see where & how txexprs are being used.
Reliability. Because txexprs come in two close but not quite equal forms, careful coders will always have to take both cases into account.
The programming is trivial, but the annoyance is real.
5 Interface
procedure
v : any/c
procedure
(txexpr-tag? v) → boolean?
v : any/c
procedure
(txexpr-attr? v) → boolean?
v : any/c
procedure
(txexpr-attr-key? v) → boolean?
v : any/c
procedure
(txexpr-attr-value? v) → boolean?
v : any/c
procedure
(txexpr-element? v) → boolean?
v : any/c
txexpr | = | (list tag (list attr ...) element ...) | ||
| | (cons tag (list element ...)) | |||
tag | = | symbol? | ||
attr | = | (list key value) | ||
key | = | symbol? | ||
value | = | string? | ||
element | = | xexpr? |
procedure
(txexpr-attrs? v) → boolean?
v : any/c
procedure
(txexpr-elements? v) → boolean?
v : any/c
procedure
(validate-txexpr possible-txexpr) → txexpr?
possible-txexpr : any/c
Examples: | ||||||||||||||||
|
procedure
v : can-be-txexpr-attr-key?
procedure
v : can-be-txexpr-attr-value?
procedure
(txexpr->values tx)
→
txexpr-tag? txexpr-attrs? txexpr-elements? tx : txexpr?
Examples: | |||||||||||||||
|
procedure
(txexpr->list tx) →
(list txexpr-tag? txexpr-attrs? txexpr-elements?) tx : txexpr?
Examples: | ||||||
|
procedure
(xexpr->html x) → string?
x : xexpr?
Examples: | |||||||||
|
procedure
(get-tag tx) → txexpr-tag?
tx : txexpr?
procedure
(get-attrs tx) → txexpr-attr?
tx : txexpr?
procedure
(get-elements tx) → (listof txexpr-element?)
tx : txexpr?
Examples: | ||||||
|
procedure
(make-txexpr tag [attrs elements]) → txexpr?
tag : txexpr-tag? attrs : txexpr-attrs? = empty elements : txexpr-elements? = empty
Examples: | |||||||||||||||
|
procedure
(can-be-txexpr-attrs? v) → boolean?
v : any/c
procedure
(attrs->hash x ...) → hash?
x : can-be-txexpr-attrs?
procedure
(hash->attrs h) → txexpr-attrs?
h : hash?
Examples: | |||||||
|
procedure
(attrs-have-key? attrs key) → boolean?
attrs : (or/c txexpr-attrs? txexpr?) key : can-be-txexpr-attr-key?
Examples: | |||||||
|
procedure
(attr-ref tx key) → txexpr-attr-value?
tx : txexpr? key : can-be-txexpr-attr-key?
Examples: | ||||||
|
procedure
tx : txexpr? key : can-be-txexpr-attr-key? value : txexpr-attr-value?
Examples: | |||||||||
|
procedure
(merge-attrs attrs ...) → txexpr-attrs?
attrs : (listof can-be-txexpr-attrs?)
You can pass the attributes in multiple forms. See can-be-txexpr-attrs? for further details.
Attributes with the same name are merged, with the later value taking precedence (i.e., hash behavior).
Attributes are sorted in alphabetical order.
Examples: | |||||||||||||||||||
|
procedure
(remove-attrs tx) → txexpr?
tx : txexpr?
Examples: | |||||
|
procedure
(map-elements proc tx) → txexpr?
proc : procedure? tx : txexpr?
Examples: | ||||||||||
|
In practice, most xexpr-elements are strings. But woe befalls those who pass string procedures to map-elements, because an xexpr-element can be any kind of xexpr?, and an xexpr? is not necessarily a string.
Examples: | ||||||||||||
|
procedure
(map-elements/exclude proc tx exclude-test) → txexpr?
proc : procedure? tx : txexpr? exclude-test : (txexpr? . -> . boolean?)
Examples: | ||||||||||
|
Be careful with the wider consequences of exclusion tests. When exclude-test is true, the txexpr is excluded, but so is everything underneath that txexpr. In other words, there is no way to re-include (un-exclude?) elements nested under an excluded element.
Examples: | ||||||||||||
|
procedure
(splitf-txexpr tx pred) →
txexpr? (listof txexpr-element?) tx : txexpr? pred : procedure?
Examples: | ||||||||||
|
6 License & source code
This module is licensed under the LGPL.
Source repository at http://github.com/mbutterick/txexpr. Suggestions & corrections welcome.