Data-Binding In Emacs Lisp: let-alist When Processing JSON Data
1 Summary
Module json-read consumes JSON data structures and transforms them
into their elisp equivalent, where JSON dictionaries become alists and
JSON arrays become vectors. Accessing that data from lisp would
ordinarily require using lisp accessors such as assoc, car and
cdr. With let-alist, we get data-binding for free — the result
is elisp code that uses dotted-variables to directly access specific
slots in a deeply nested data structure. Thus, processing data
available as JSON via Web APIs is a really good use-case for
let-alist. Long-standing wish — I wish Emacs' JSON parsing were
implemented in native code rather than in elisp.
1.1 A Working Example
I recently implemented myself a NOAA Weather API Client — it pulls
the NOAA Weather Forecast (weekly and hourly) as JSON objects, and
produces an org-mode buffer that renders the data.
Note that though the above is part of a much larger
emacspeak-wizards module, the above function and its dependencies
are themselves mostly independent of Emacspeak, except for the last
two forms in the weather forecast function.
Here is an annotated version of the function that gets NOAA data and
leverages let-alist to process the results:
(defun ems--noaa-get-data (ask) "Internal function that gets NOAA data and returns a results buffer." (declare (special gweb-my-address)) (let* ((buffer (get-buffer-create "*NOAA Weather*")) (inhibit-read-only t) (date nil) (start (point-min)) (address (when ask (read-from-minibuffer "Address:"))) (geo (when ask (gmaps-geocode address)))) (unless address (setq address gweb-my-address)) (with-current-buffer buffer (erase-buffer) (special-mode) (orgstruct-mode) (setq header-line-format (format "NOAA Weather For %s" address)) (insert (format "* Weather Forecast For %s\n\n" address)) ;;; produce Daily forecast (let-alist (g-json-from-url (ems--noaa-url geo)) (cl-loop for p across .properties.periods do (let-alist p (insert (format "** Forecast For %s: %s\n\n%s\n\n" .name .shortForecast .detailedForecast))) (fill-region start (point))) (insert (format "\nUpdated at %s\n" (ems--noaa-time "%c" .properties.updated)))) (let-alist ;;; Now produce hourly forecast (g-json-from-url (concat (ems--noaa-url geo) "/hourly")) (insert (format "\n* Hourly Forecast:Updated At %s \n" (ems--noaa-time "%c" .properties.updated))) (cl-loop for p across .properties.periods do (let-alist p (unless (and date (string= date (ems--noaa-time "%x" .startTime))) (insert (format "** %s\n" (ems--noaa-time "%A %X" .startTime))) (setq date (ems--noaa-time "%x" .startTime))) (insert (format " - %s %s %s: Wind Speed: %s Wind Direction: %s\n" (ems--noaa-time "%R" .startTime) .shortForecast .temperature .windSpeed .windDirection))))) (goto-char (point-min))) buffer))
- In the above_ /gweb-my-address_ is a Lat/Lng pair as returned by
gmaps-geocode defined in g-client/gmaps.el. That is used as the
default location for which we retrieve the forecast. - Parameter ask if non-nil results in the user being prompted
for the address — that address is then geocoded using
the Google Maps API. - The weather forecast display will leverage org-mode for
structured navigation; however we dont want that buffer to be
editable in general; moreover special-mode gives us nice
features such as q for quitting that window. So we use
special-mode as the major mode, and orgstruct-mode as a minor
mode to get the best of both worlds. - The API call to NOAA results in a JSON data structure where
result.properties.periods holds an array of forecast
objects. Using that result in let-alist gives us data binding
for free! Notice the following:
- We can use .properties.periods in the cl-loop as the list
to iterate over. - Within that loop body, a second let-list enables data
binding over the forecast object that we are processing in the
loop body. - Data accesses inside the loop body are again simple given the
data binding created by the let-alist.
- We can use .properties.periods in the cl-loop as the list
The code for generating the hourly forecast is similar in spirit —
the main take-away here is that let-alist saves a lot of
boiler-plate code that would have been otherwise required to take
apart the nested list structure we got back with our data.