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))
  1. 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.
  2. Parameter ask if non-nil results in the user being prompted for the address — that address is then geocoded using the Google Maps API.
  3. 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.
  4. 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:
    1. We can use .properties.periods in the cl-loop as the list to iterate over.
    2. Within that loop body, a second let-list enables data binding over the forecast object that we are processing in the loop body.
    3. Data accesses inside the loop body are again simple given the data binding created by the let-alist.

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.

Date: 2017-07-27 Thu 00:00

Author: T.V Raman

Created: 2017-07-27 Thu 14:03

Validate