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 or lines. More on that later.

#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. 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 · vector · lines

Here, the indices of not-typically-thought-of-as-kv things are taken as keys.

list

For simple lists, we want to use the specific conversion function (h<-list). This is because h<-it will try to match cons cell, list of lists, alist, and plist first. So we avoid using autodetection in this case.

(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

cons cell

Too simple, but there you go:

(let ((thing '(Alice . 42)))
  (h-let-it thing
    .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.

It 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="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 "local 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 simply:

(-> "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

So there you have it

Simple, easy-to-remember recipes to dot-bind all sorts of key–value collections in Emacs Lisp.