Dot-bind any key–value thing in Emacs Lisp
Introduction
You know let-alist
, right?
(let-alist '((a . Alice) (b . ((ba . Barbara) (bo . Bob)))) .b.bo) => 'Bob
I find that very nice. So I wrote h-let
, a macro that does the same thing to hash tables:
(h-let (h* 'a 'Alice 'b (h* 'ba 'Barbara 'bo 'Bob)) .b.bo) => 'Bob
You find h-let
in XHT, which also happens to have a function called h<-it
. What does it do?
Basically, if something walks like a key–value thing, quacks like a key–value thing, or can anyhow be coerced to behave as if it were, perhaps, a key–value thing — why, then h<-it
is sure to swiftly convert that key–value thing into a hash table!
Now guess what happens when we compose h-let
with h<-it
?
Let me show you. We start with the simplest, most boring types. Then we raise the dimension, and it gets more interesting.
The recipes
There are three ways to do it.
I exemplify it below with a plist-ified version of the previous alist (to vary the examples a bit).
#1: composing h-let with the specific conversion function
(let ((thing '(:a Alice :b (:ba Barbara :bo Bob)))) (h-let (h<-plist* thing) .b.bo)) => 'Bob
This is the recommended way for converting lists and lines, because although h<-it
does a great job in autodetecting the type:
- with lists we want to explicitly use
h<-list
to disambiguate it from plists: if the list has an even number of elements,h<-it
assumes it's a plist, since this is a much more common type of key–value thing to convert from - and since strings can be used to represent all sorts of other key–value things,
h<-it
will only assume that the input is about lines when nothing else matches, so if you want to make sure it's treated as lines, useh<-lines
instead, as it'll skip the type-checking and be more robust.
#2: composing h-let with h<-it, which tries to guess what it is
(let ((thing '(:a Alice :b (:ba Barbara :bo Bob)))) (h-let (h<-it thing) .b.bo)) => 'Bob
#3: using h-let-it, which does the previous with a single function
(let ((thing '(:a Alice :b (:ba Barbara :bo Bob)))) (h-let-it thing .b.bo)) => 'Bob
When first writing this post, most examples used the second option — but this single function was begging to be written!
Well, I'm happy to say that it's now available. It has been pushed to xht's git repo (not yet tagged) and can already be used. So all examples below will use it, except as noted above in #1.
Dot-bindable things
With XHT you can dot-bind more than a dozen kinds of key–value things:
Dimension | Dot-bindable things |
---|---|
1D: implicit | list, vector, lines |
1D: explicit | key–value lines, cons cell |
≥1D unspecific | alist, plist, json, hash table |
2D | list of lists, org table, tsv, csv, ssv |
Let's have a look.
1D: implicit
list
With lists we use the specific conversion function to avoid ambiguity in type-detection.
(let ((thing '(Alice Bob Emily))) (h-let (h<-list thing) .\1)) => 'Bob
We need to escape the number, of course: .1 would be interpreted as just a float.
A better example:
(let ((thing '(Alice Bob Emily))) (h-let (h<-list thing) (format "%s and %s" .\0 .\1))) => "Alice and Bob"
Now, we started with the most boring of types. The above is nice, but of course there are already many other ways in Emacs Lisp to refer to list elements by their position. Here are some of these ways, for comparison:
Using car
and cadr
:
(let* ((thing '(Alice Bob Emily)) (a (car thing)) (b (cadr thing))) (format "%s and %s" a b)) => "Alice and Bob"
Using nth
:
(let* ((thing '(Alice Bob Emily)) (a (nth 0 thing)) (b (nth 1 thing))) (format "%s and %s" a b)) => "Alice and Bob"
Using elt
:
(let* ((thing '(Alice Bob Emily)) (a (elt thing 0)) (b (elt thing 1))) (format "%s and %s" a b)) => "Alice and Bob"
Using s-format
:
(let ((thing '(Alice Bob Emily))) (s-format "$0 and $1" 'elt thing)) => "Alice and Bob"
Using -let
:
(let ((thing '(Alice Bob Emily))) (-let [(a b) thing] (format "%s and %s" a b))) => "Alice and Bob"
Using -let*
:
(-let* ((thing '(Alice Bob Emily)) ((a b) thing)) (format "%s and %s" a b)) => "Alice and Bob"
Using pcase
:
(let ((thing '(Alice Bob Emily))) (pcase thing (`(,a ,b ,_) (format "%s and %s" a b)))) => "Alice and Bob"
Using pcase-let
:
(let ((thing '(Alice Bob Emily))) (pcase-let ((`(,a ,b) thing)) (format "%s and %s" a b))) => "Alice and Bob"
Using pcase-let*
:
(pcase-let* ((thing '(Alice Bob Emily)) (`(,a ,b) thing)) (format "%s and %s" a b)) => "Alice and Bob"
Using cl-destructuring-bind
:
(let ((thing '(Alice Bob Emily))) (cl-destructuring-bind (a b _) thing (format "%s and %s" a b))) => "Alice and Bob"
So use whichever you like.
Now, if the list is large and the elements you want are far down the road, these last five ways can be a bit awkward to use. So direct reference to the index seems better. For example:
Using h-let
:
(let ((thing '(a b c d e f g h i j k l m n o p q r s t u v w x y z))) (h-let (h<-list thing) (format "%s and %s" (s-titleize (format "%s%s%s%s%s" .\0 .\11 .\8 .\2 .\4)) (s-titleize (format "%s%s%s" .\1 .\14 .\1))))) => "Alice and Bob"
Using s-format
:
(let ((thing '(a b c d e f g h i j k l m n o p q r s t u v w x y z))) (format "%s and %s" (s-titleize (s-format "$0$11$8$2$4" 'elt thing)) (s-titleize (s-format "$1$14$1" 'elt thing)))) => "Alice and Bob"
Using nth
:
(let ((thing '(a b c d e f g h i j k l m n o p q r s t u v w x y z)) (a-idx '(0 11 8 2 4)) (b-idx '(1 14 1))) (-let [(a b) (-map (lambda (idx) (->> (--map (symbol-name (nth it thing)) idx) (s-join "") (s-titleize))) `(,a-idx ,b-idx))] (format "%s and %s" a b))) => "Alice and Bob"
Using -select-by-indices
:
(let ((thing '(a b c d e f g h i j k l m n o p q r s t u v w x y z)) (a-idx '(0 11 8 2 4)) (b-idx '(1 14 1))) (-let [(a b) (-map (lambda (idx) (->> (-select-by-indices idx thing) (-map #'symbol-name) (s-join "") (s-titleize))) `(,a-idx ,b-idx))] (format "%s and %s" a b))) => "Alice and Bob"
Anyway, enough of lists. There are too many ways to deal with them.
vector
Same thing. But there's no ambiguity, so we can use either h<-vector
or h<-it
:
(let ((thing [Alice Bob Emily])) (h-let-it thing (format "%s and %s" .\0 .\1))) => "Alice and Bob"
Although vectors are far less common than lists, there are also other ways to deal with them.
The most obvious alternative would be aref
, which is like elt
for vectors, so it also uses indices:
(let* ((thing [Alice Bob Emily]) (a (aref thing 0)) (b (aref thing 1))) (format "%s and %s" a b)) => "Alice and Bob"
Some of the previous ones also work here.
Using s-format
:
(let ((thing [Alice Bob Emily])) (s-format "$0 and $1" 'elt thing)) => "Alice and Bob"
Using -let
:
(let ((thing [Alice Bob Emily])) (-let [[a b] thing] (format "%s and %s" a b))) => "Alice and Bob"
Using -let*
:
(-let* ((thing [Alice Bob Emily]) ([a b] thing)) (format "%s and %s" a b)) => "Alice and Bob"
lines
Here we have lines of text, the first one being indexed as 0.
(let ((thing "Alice\nBob\nEmily")) (h-let-it thing (format "%s and %s" .\0 .\1))) => "Alice and Bob"
or:
(let ((thing "\ Alice Bob Emily")) (h-let-it thing (format "%s and %s" .\0 .\1))) => "Alice and Bob"
Note that strings can be used to represent all sorts of other key–value things, and h<-it
will assume that the input is about lines only when nothing else matches. If you want to make sure it's treated as lines, use h<-lines
instead, as it'll skip the type-checking and be more robust. For example, here it would be needed:
(let ((thing "\ Alice = 42 Bob = 30 Emily = 21")) (h-let (h<-lines thing) .\2)) => "Emily = 21"
because otherwise h<-it
would autodetect the thing as representing key–value lines. Speaking of which:
1D: explicit
key–value lines, cons cell |
These are explicitly 1D: no nesting allowed, just simple values.
kvl (key–value lines)
(let ((thing "\ Alice = 42 Bob = 30")) (h-let-it thing .Alice)) => "42"
When dealing with a KVL whose delimiter doesn't match " *= *", we can do this:
(let ((thing "Alice:42\nBob:30") (h-kvl-sep-re " *: *")) (h-let-it thing .Alice)) => "42"
The result from these will always be strings.
If we prefer it in another format, we can use one of the library's h-as-X
functions:
(let ((thing "Alice=42\nBob=30")) (h-let-it thing (h-as-number .Alice))) => 42
≥1D unspecific
alist, plist, json, hash table |
These come in many forms:
Dimension | How it looks | Values of the same type? |
---|---|---|
1D | simple | no, none |
1.5D | partially nested | yes, at least some |
≥2D | tabular | yes, all (plus some conditions) |
We can dot-bind them regardless.
In the examples here, they are partially nested.
alist (association list)
This is the same alist of the opening let-alist
example:
(let ((thing '((a . Alice) (b . ((ba . Barbara) (bo . Bob)))))) (h-let-it thing .b.bo)) => 'Bob
But let-alist
doesn't yet work with keywords or strings, whereas h-let
and h-let-it
do:
(let ((thing '((:a . Alice) (:b . ((:ba . Barbara) (:bo . Bob)))))) (h-let-it thing .b.bo)) => 'Bob
(let ((thing '(("a" . "Alice") ("b" . (("ba" . "Barbara") ("bo" . "Bob")))))) (h-let-it thing .b.bo)) => "Bob"
plist (property list)
Using the same example, here are the corresponding plists with:
keys as symbols:
(let ((thing (list 'a 'Alice 'b (list 'ba 'Barbara 'bo 'Bob)))) (h-let-it thing .b.bo)) => 'Bob
keys as keywords:
(let ((thing (list :a 'Alice :b (list :ba 'Barbara :bo 'Bob)))) (h-let-it thing .b.bo)) => 'Bob
and keys and values as strings
(and let's also turn the resulting "Bob" into a keyword):
(let ((thing (list "a" "Alice" "b" (list "ba" "Barbara" "bo" "Bob")))) (h-let-it thing (h-as-keyword .b.bo))) => :Bob
json (javascript object notation)
The same recipe also works with JSON:
(let ((thing "{ \"a\": \"Alice\", \"b\": { \"ba\": \"Barbara\", \"bo\": \"Bob\" } }")) (h-let-it thing .b.bo)) => "Bob"
hash table
XHT is all about hash tables — so we can do them, too, of course:
(let ((thing (h* 'a 'Alice 'b (h* 'ba 'Barbara 'bo 'Bob)))) (h-let-it thing .b.bo)) => 'Bob
Naturally, with hash tables we can drop the it
:
(let ((thing (h* 'a 'Alice 'b (h* 'ba 'Barbara 'bo 'Bob)))) (h-let thing .b.bo)) => 'Bob
You can pass them in other available formats as well. Here's the same one as (ht…)
:
(let ((thing (ht ('a 'Alice) ('b (ht ('ba 'Barbara) ('bo 'Bob)))))) (h-let thing .b.bo)) => 'Bob
And here's the same one in that ungainly native format:
(let ((thing #s(hash-table size 2 test equal data (a Alice b #s(hash-table size 2 test equal data (ba Barbara bo Bob)))))) (h-let thing .b.bo)) => 'Bob
A small pause to praise xht-do
.
Think I typed these last two alternative hash table forms from scratch? Nope.
I copied the original (h*…)
example, put point at the end of the sexp, and launched xht-do
.
Then I pressed c x r h 2, meaning:
- convert
- last sexp
- replace
- to hash table
- 2:
(ht…)
pretty-printed
and for the last one: same thing but 3, with some extra trimming.
The xht-do
dispatcher can convert from and to all the more than a dozen key–value types shown here, and automatically detects dimensions, suggests only licit options, and gives different output choices regarding format (such as pretty-printing) and destination (replace, at point, to buffer, to file, to clipboard, etc.).
Try it. (Think I typed that JSON?)
Ok, now let's see the two-dimensional ones.
2D
list of lists, org table, tsv, csv, ssv |
These look like tabular structures.
Their first row must always be a header.
lol (list of lists)
(let ((thing '((name age editor) (Alice 42 Emacs) (Bob 30 Vim) (Emily 21 Nano)))) (h-let-it thing .Alice.age)) => 42
(let ((thing '((name age editor) (Alice 42 Emacs) (Bob 30 Vim) (Emily 21 Nano)))) (h-let-it thing (format "%s is %s and prefers %s." "Alice" .Alice.age .Alice.editor))) => "Alice is 42 and prefers Emacs."
org table
Org tables can be easily looked up and dot-bound, too:
(let ((thing "| name | age | editor | +-------+-----+--------| | Alice | 42 | Emacs | | Bob | 30 | Vim | | Emily | 21 | Nano |")) (h-let-it thing (h-as-number .Alice.age))) => 42
tsv (tab-separated values)
TSVs are also a breeze:
(let ((thing "name\tage\nAlice\t42\nBob\t30")) (h-let-it thing .Alice.age)) => "42"
Doesn't the above look almost simpler than doing it in Bash?
local thing thing="name\tage\nAlice\t42\nBob\t30" echo -e "$thing" | grep ^Alice | cut -f2
Which, by the way, can be shortened to:
: "name\tage\nAlice\t42\nBob\t30" # with ":" + "$_", who needs "thing="? <<<"${_@E}" grep ^Alice | cut -f2 # with "<<<" + "@E", who needs "echo -e"?
Actually, we can get rid of the "thing" in Emacs Lisp as well:
(--> "name\tage\nAlice\t42\nBob\t30" (h-let-it it ;<--- we're literally h-letting it! .Alice.age)) => "42"
Or even:
(-> "name\tage\nAlice\t42\nBob\t30" (h-let-it .Alice.age)) => "42"
csv (comma-separated values)
These work, too.
(let ((thing "name,age\nAlice,42\nBob,30")) (h-let-it thing .Alice.age)) => "42"
ssv (space-separated values)
TABs make excellent field separators.
Commas are not a great choice, but are ok in simpler cases.
And 2+ spaces are often fine: space-separated values. These are exactly what you get as output from Bash commands such as this one:
column -ts, <<< "\ name,age,editor Alice,42,Emacs Bob,30,Vim Emily,21,Nano"
And would you believe that these can be easily dot-bound as well?
(let ((thing "\ name age editor Alice 42 Emacs Bob 30 Vim Emily 21 Nano")) (h-let-it thing (format "%s is %s and prefers %s." "Alice" .Alice.age .Alice.editor))) => "Alice is 42 and prefers Emacs."
What if we need to read the data from a file instead?
XHT has several h-read-X
functions, where X is one of the types we've seen above.
With them, we can read files without having to think too much whether the thing we've just read needs extra quoting, unquoting, interning, symbol-naming, format-%S-ing, prin1-to-string-ing, or whatnot.
(--> "/path/to/that/alice-and-bob.tsv" h-read-tsv h<-tsv ; or h<-it, either one is fine (h-let it .Alice.age) h-as-number) => 42
Prefer it compact?
(-> "/path/to/that/alice-and-bob.tsv" h-read-tsv (h-let-it .Alice.age) h-as-number) => 42