reddit / emacs

Wrapping request.el

sauntcartas · 2022-05-09 21:16
I wanted to share a nifty little Emacs Lisp macro I put together recently. Previously, I'd been making REST API requests using Emacs's built-in `url-retrieve-synchronously` function, which generates a brand-new response buffer. I wrote a resource-managing macro for it that cleans up the buffer after setting point to the start of the response body and executing the macro body there, returning as its value the last expression in the body. For example: (with-synchronous-download "https://api.github.com/.../jobs" (loop for job across (json-read) collect (job-summary job))) It worked well enough, but in order to do things like send non-GET requests, include a request body or extra headers, etc, I had to bind global variables like `url-request-method`, `url-request-data`, etc. It just wasn't very readable, and there was little in the way of error handling. Eventually, I decided to try using request.el. I found that request.el expects to do its work via side effect in a callback, but I wanted a synchronous parsed response. I eventually produced this macro wrapper (specialized for Github): (defmacro github-request (path &rest body) (let ((result (make-symbol "result"))) `(let (,result) (request (format "https://api.github.com/repos/mycompany/%s" ,path) :sync t :headers (my-github-headers) :parser (lambda () ,@body) :success (lambda (&rest plist) (setq ,result (plist-get plist :data)))) ,result))) This worked fine for simple GET requests, but I was stumped at first how I might do POSTs or provide a request body. I couldn't just bind globals as I had before (and didn't want to), those would have to be additional keyword arguments passed to the `request` function. Somehow I needed to pass the keywords to the macro. I knew that I could use `cl-defmacro` to get `&key` parameters for it, but I also needed a `&rest` parameter for the body, and I knew from occasional bitter experience that those two parameter types don't mix as one might naively expect. Searching around for advice, I found a page somewhere that bluntly stated that keyword parsing had to be done manually, so that's what I did. It took a few iterations to get a nice functional implementation, but this is what I ended up with: (defmacro github-request (path &rest keys-and-body) (letrec ((result (make-symbol "result")) (parse (lambda (list) (pcase list (`(,(and (pred keywordp) key) ,value . ,rest) `(,@(funcall parse rest) ,key ,value)) (body `(:parser (lambda () ,@body))))))) `(let (,result) (request (format "https://api.github.com/repos/mycompany/%s" ,path) :sync t :headers (my-github-headers) :success (lambda (&rest plist) (setq ,result (plist-get plist :data))) ,@(funcall parse keys-and-body)) ,result))) I discovered `letrec` while creating this macro, and decided to use it instead of a `let` to create the `result` symbol and a separate, nested `cl-labels` to define the recursive `parse` function, even at the expense of needing an extra `funcall` in two places. Anyway, now I can do things like: (github-request ".../jobs" :method "POST" :data '(("foo" . "bar")) (loop for job across (json-read) collect (job-summary job)))
Phil-Hudson · 2022-05-11 10:35
This is opening up areas of Elisp that I have only a vague grasp on. I've just been thru the doco and not much wiser on the differences between `make-symbol` and `intern` -- really can't see why this is the right thing to do here, and I always use either `intern` or some variant of `gensym` in similar macro code myself -- and between `letrec` and `let*`. The latter seems particularly baffling: what could it even mean for a symbol to be bound before the value it is (going to be? when?) bound to has been eval'ed? I'd appreciate it if you have a moment to help me understand.
sauntcartas · 2022-05-11 17:23
As for `make-symbol` versus `gensym`, the only difference I know of is that the former lets you name the symbol, while the latter generates a name automatically, and by default it looks something like `g1766`. I've come to prefer `make-symbol` because if I ever look at a macro expansion, the names I assign to new symbols are more readable than the ones created by `gensym`. You can find `letrec`'s source code from within Emacs. It's short and relatively readable, and transforms this: (letrec ((foo (foo-value)) (bar (bar-value))) body-forms...) ...into this: (let (foo bar) (setq foo (foo-value)) (setq bar (bar-value)) body-forms...) Here, if `(foo-value)` is some expression that refers to `foo`, it will refer to the `foo` that was just bound. With a regular `let`, `foo` would refer to the `foo` from the outer scope, if any. (That's with lexical binding in effect. I'd have to think about how dynamic binding would behave.) Consider this: (setq countdown (lambda (timer) (insert "fizzle"))) (letrec ((countdown (lambda (timer) (if (zerop timer) (insert "BOOM") (insert (number-to-string timer) "\n") (funcall countdown (1- timer)))))) (funcall countdown 10)) If you evaluate these forms in the `*scratch*` buffer, you'll see a countdown from 10 followed by a BOOM. If you change the `letrec` to `let` and evaluate them again, you'll see 10 followed by "fizzle".
Phil-Hudson · 2022-05-11 17:54
Thank you! I get it now. Bound to nil to start with. Of course. It all makes sense now. And what an excellent demo!