hoowl

Physicist, Emacser, Digitales Spielkind

Elisp: Splicing conditional argument lists to pass to function calls
Published on Aug 03, 2022.

Today, I found out about the special marker ,@ in Elisp: it allows to splice an evaluated value into a quoted list while keeping the level of the “injected” elements the same as other elements on the resulting list. This solved (or at least simplified) a problem I faced a couple of times already: whenever I wanted to assemble arguments for functions that have a &rest ARGS in their signature, such as call-process, using conditionals and evaluated functions, the code quickly became more complicated or duplicative than felt right to me. Let me give you an example.

call-process uses the signature call-process PROGRAM &optional INFILE DESTINATION DISPLAY &rest ARGS. All the arguments ARGS to the called program have to be given as individual strings. So for a call to the command line tool find to look for files matching the pattern elisp*.org in the current directory that have been modified today this could look like:

(with-temp-buffer
  (call-process
   "find" nil t nil
   "./"
   "-mtime" "0"
   "-name" "elisp*.org")
  (buffer-substring (point-min) (point-max)))
./elisp_splicing_conditional_argument_lists_to_pass_to_function_calls.org

Here, with-temp-buffer and buffer-substring are used to process the output of the command. Note that -name- and elisp*.org are separate strings despite the latter giving the pattern to the argument name – anything else, and find would not recognize the argument as valid. The same goes for the -mtime argument and its parameter.

I found it difficult to provide arguments in this particular format when I want to fill the values from variables and make some of them optional. Consider this example where the result should be a -name and pattern but no mtime arguments to find:

(let ((pattern "elisp*.org")
      (n nil))
  `(,(when pattern
       `("-name" ,pattern))
    ,(when n
       `("-mtime" ,n)))))
(("-name" "elisp*.org") nil)

This approach almost works, but the result is a nested list and a nil value which we have to remove before calling call-process. This can be easily done by introducing the ,@ special marker:

(let* ((pattern "elisp*.org")
      (n nil))
  `(,@(when pattern
        `("-name" ,pattern))
    ,@(when n
        `("-mtime" ,n)))))
("-name" "elisp*.org")

Now the result is in the right format and the code had to be changed only minimally! Final puzzle piece: feed the arguments to call-process using apply #'call-process args:

(let* ((pattern "elisp*.org")
      (n nil))
  (with-temp-buffer
    (apply #'call-process
           `("find" nil t nil
             ,@(when pattern
                 `("-name" ,pattern))
             ,@(when n
                 `("-mtime" ,n))))
    (buffer-substring (point-min) (point-max))))
./elisp_splicing_conditional_argument_lists_to_pass_to_function_calls.org

Which – to my eyes – is compact and reasonably readable code 😸

But feel free to get in touch and tell me otherwise (preferably with suggestions for improvements)!

Tags: elisp