0

I'm writing a function which wraps org-export-as for use in html conversion of a file. In this function, I define the options for org-export-with-toc, org-export-with-section-numbers, and org-html-htmlize-output-type based on the optional arguments provided to the user. If the user fails to provide an argument, I supply a default.

(defun my-export (file &optional toc section-num output-type backend)
  "Export FILE to html string using `org-export-as'.             

This function wraps `org-export-as'.  See that function for greater argument
details.

TOC and SECTION-NUM generate table of contents and section
numbers, respectively.  Defaults for each are nil.

OUTPUT-TYPE is 'css, 'inline-css, or nil as defined by
`org-html-htmlize-output-type'.  Default is 'css.

BACKEND is the export backend.  Default is 'html."
  (let* ((org-export-with-toc toc)
         (org-export-with-section-numbers section-num)
         (backend (or backend 'html))
         ;; Want 'css to be the default value here
         (org-html-htmlize-output-type
          (find output-type '(css inline-css nil))))
         (converted
          (with-temp-buffer
            (insert-file-contents-literally file)
            (org-export-as backend nil nil t nil))))                                                                                                               converted))

The trouble is this: org-html-htmlize-output-type only accepts three values, 'css, 'inline-css, or nil. However, when a user fails to provide an optional argument, nil is passed. I have no way to discern if nil was provided intentionally as the preferred OUTPUT-TYPE or if it was simply ignored in favor of the default!

How is such a dilemma commonly handled?

One approach is to create a new value to represent nil and use cond to filter for the various choices:

(defun my-export (file &optional toc section-num output-type backend)
  (let* ((org-export-with-toc toc)
         (org-export-with-section-numbers section-num)
         (backend (or backend 'html))
         ;; To toggle nil, user must specify 'plain-text
         (org-html-htmlize-output-type
          (cond ((eq output-type 'css) type)
                ((eq output-type 'inline-css) type)
                ((eq output-type 'plain-text) nil)
                ('css)))
         (converted
          (with-temp-buffer
            (insert-file-contents-literally file)
            (org-export-as backend nil nil t nil))))
    converted))

Another thought I had was to use 'nil instead of 'plain-text but (un)fortunately (eq nil 'nil) is t.


Timing all the options, they are all on par.

(defun test-export-cond (file &optional arg1 arg2 arg3 opt4)
  "Test export using cond."
  (let* ((opt1 arg1)
         (opt2 arg2)
         (opt3 (cond ((eq arg3 'css) type)
                ((eq arg3 'inline-css) type)
                ((eq arg3 'plain-text) nil)
                ('css)))
         (opt4 (or opt4 'html)))
    (list file opt1 opt2 opt3 opt4)))

(defun test-export-if (file &rest rargs)
  "Test export using if."
  (let* ((nargs (length rargs))
         (opt1 (nth 0 rargs))
         (opt2 (nth 1 rargs))
         (opt3
          (if (< nargs 3)
              'css
            (nth 2 rargs)))
         (opt4
          (if (< nargs 4)
              'html
            (nth 3 rargs))))
    (list file opt1 opt2 opt3 opt4)))

(cl-defun test-export-cl (file &optional arg1 arg2 (arg3 'css) (opt4 'html))
  "Test export using cl-lib."
  (let* ((opt1 arg1)
         (opt2 arg2)
         (opt3 arg3))
    (list file opt1 opt2 opt3 opt4)))

(test-export-cond "~/file.txt")
(test-export-if "~/file.txt")
(test-export-cl "~/file.txt")

(defmacro test-measure-time (times &rest body)
  "Measure the average time it takes to evaluate BODY."
  `(let ((cur-time (current-time)))
     (dotimes (i ,times)
       ,@body)
     (message "%.06f" (/ (float-time (time-since cur-time))) ,times)))

(setq test-do-times 1000000)

(test-measure-time test-do-times (test-export-cond "~/file.txt"))  ; "0.063467"
(test-measure-time test-do-times (test-export-if "~/file.txt"))  ; "0.064669"
(test-measure-time test-do-times (test-export-cl "~/file.txt"))  ; "0.066125"
  • Please reword a bit, to not ask for the "best" way but just for a way. You'll get answers that propose different ways, and you can decide which helps you most (is "best" for you). Questions shouldn't be primarily opinion-based, and "best" kind of invites that. Thx. – Drew Feb 22 at 0:35
3

A simple way to differentiate between value nil and a missing optional argument is to use &rest instead of &optional. I demonstrate that with the following test function:

(defun testfun (arg &rest optArgs)
  "Do something with ARG, OPT1, and OPT2.
OPT1 and OPT2 can be nil, 1, and 2.
The default of OPT1 is 1 and the default of 2 is 2.

\(fn ARG &optional OPT1 OPT2)"
  (let* ((nOpt (length optArgs))
     (opt1 (if (< nOpt 1)
           1
         (nth 0 optArgs)))
     (opt2 (if (< nOpt 2)
           2
         (nth 1 optArgs))))
    (list arg opt1 opt2)))

Note also the \(fn ARG &optional OPT1 OPT2) in the doc string. That replaces the actual implementation (testfun ARG &rest OPTARGS) in the help buffer generated by describe-function. In this way the user is not bothered with the implementation details but directly informed about the meaning.

Test 1: Set opt2 explicitly to nil:

(testfun 1 'a nil)

(1 a nil)

Test 2: Setting opt1 and opt2 explicitly to nil:

(testfun 1 nil nil)

(1 nil nil)

Test 3: Setting opt1 explicitly to nil and using default opt2:

(testfun 1 nil)

(1 nil 2)

Test 4: Only using default values:

(testfun 1)

(1 1 2)

That is the way if you do not want to draw in cl-lib. But, wait... I will provide another solution with cl-lib.

| improve this answer | |
  • What is (fn ARG &optional OPT1 OPT2) in your docstring? I notice that edebug-defun gets hung up on it. Is it not just a helpful reference to the &optional form of the function definition? – Lorem Ipsum Feb 21 at 23:42
  • 1
    @LoremIpsum I added a note on the meaning of (fn ARG &optional OPT1 OPT2) in the answer. – Tobias Feb 22 at 0:28
  • 1
    @LoremIpsum Changing that to \(fn...) avoids edebug issues and the like. Note that this applies to any docstring line starting with (. Moreover, any line anywhere in an elisp library beginning with ( which is not some kind of top-level definition is liable to cause problems. See C-h i g (elisp)Documentation Tips and (emacs)Left Margin Paren. – phils Feb 22 at 1:33
2

cl-defun from cl-macs.el allows you to specify default values (beside much other mind-blowing stuff).

(cl-defun testfun (arg &optional (opt1 1) (opt2 2))
  "Process normal ARG and optional args OPT1 and OPT2 with defaults 1 and 2, respectively."
  (list arg opt1 opt2))

Test 1: Set opt2 explicitly to nil:

(testfun 1 'a nil)

(1 a nil)

Test 2: Setting opt1 and opt2 explicitly to nil:

(testfun 1 nil nil)

(1 nil nil)

Test 3: Setting opt1 explicitly to nil and using default opt2:

(testfun 1 nil)

(1 nil 2)

Test 4: Only using default values:

(testfun 1)

(1 1 2)

| improve this answer | |
1

The cleanest way IMO is to define an additional variable in the argument list that indicates if the argument was supplied by the caller. This can be done using cl-defun since it supports Common Lisp style argument lists. For example,

(require 'cl-macs)

(cl-defun test (&optional (x nil supplied-p))
  (list x supplied-p))

(test) => (nil nil)
(test 1) => (1 t)
(test nil) => (nil t)
| improve this answer | |
  • You do not need the explicit (require 'cl-macs). It is autoloaded. – Tobias Feb 21 at 21:00
  • @Tobias On my system (Emacs 26 on Linux), I get an error when (require 'cl-macs) is omitted. It's true that you can require 'cl-lib instead of 'cl-macs, however. – Qudit Feb 21 at 21:02
  • 1
    Okay to be more specific: You do not need the explicit (require 'cl-macs) if your code is byte-compiled before it is evaluated. E.g., "Byte Compile and Load" works without (require 'cl-macs). – Tobias Feb 21 at 21:06
  • @Tobias That's interesting. I didn't know bytecompilation behaved differently in the regard. I'll have to look into it more. – Qudit Feb 21 at 21:09
  • I am also not sure how cl-macs gets autoloaded when cl-defun is used on byte compilation. I therefore started a new question. – Tobias Feb 24 at 5:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.