1

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"
Lorem Ipsum
  • 4,327
  • 2
  • 14
  • 35
  • 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 '20 at 00:35

3 Answers3

4

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.

phils
  • 48,657
  • 3
  • 76
  • 115
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • 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 '20 at 23:42
  • 1
    @LoremIpsum I added a note on the meaning of `(fn ARG &optional OPT1 OPT2)` in the answer. – Tobias Feb 22 '20 at 00: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 '20 at 01: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)

Tobias
  • 32,569
  • 1
  • 34
  • 75
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)
Qudit
  • 828
  • 8
  • 16
  • You do not need the explicit `(require 'cl-macs)`. It is autoloaded. – Tobias Feb 21 '20 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 '20 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 '20 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 '20 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](https://emacs.stackexchange.com/q/55738/2370). – Tobias Feb 24 '20 at 05:59
  • @Tobias I see. Seems like the fact that byte compilation works without the require is a quirk of the implementation and shouldn't be relied upon. – Qudit Feb 24 '20 at 18:13