3

Consider the following form:

(pcase '(:def "k" #'foo :wk "ho")
  (`(:def
     ,(and (or (pred stringp) (pred vectorp)) key)
     ,(and (pred xl-sharp-quoted-symbol-p) def)
     :wk
     ,(and (pred stringp) desc))
   (format "%s | %s | %s" key def desc)))
;; =>
"k | #'foo | ho"

Suppose that I want the keywords :def and :wk to be optional. In other words I want the pcase form to successfully parse the following lists:

'("k" #'foo "bar")
'("k" #'foo :wk "bar")
'(:def "k" #'foo "bar")

Of course I realize it is possible to copy over the clause in my first example in all possible combinations omitting either :def, :wk or both. However, that's tedious and duplicates much code.

I wish I could do something like this:

(pcase '(:def "k" #'foo :wk "ho")
  (`(,(optional :def)
     ,(and (or (pred stringp) (pred vectorp)) key)
     ,(and (pred xl-sharp-quoted-symbol-p) def)
     ,(optional :wk)
     ,(and (pred stringp) desc))
   (format "%s | %s | %s" key def desc)))

Is there a way to do this concisely with pcase? If so, how?

Drew
  • 75,699
  • 9
  • 109
  • 225
Aquaactress
  • 1,393
  • 8
  • 11

2 Answers2

2

The backquote pcase pattern `--pcase-macroexpander is implemented using pcase-defmacro in the library pcase.el .

There is no pattern included there that does what you discussed. But, it may be added as shown in the following Elisp code that defines an alternative pcase pattern bq-opt.

The added lines are cleanly added as one block. This block is framed by two lines only consisting of semi-colons.

(pcase-defmacro bq-opt (qpat)
  "Pattern (bq-opt QPAT) dealing with ?\\, like the backquote pattern.
QPAT can take the following forms:
  ((optional QPAT1) . QPAT2)  matches if QPAT1 matches the car
                            and QPAT2 matches the cdr or if QPAT2 matches
  (QPAT1 . QPAT2)           matches if QPAT1 matches the car and QPAT2 the cdr.
  ,PAT                      matches if the `pcase' pattern PAT matches.
  SYMBOL                    matches if EXPVAL is `equal' to SYMBOL.
  KEYWORD                   likewise for KEYWORD.
  NUMBER                    likewise for NUMBER.
  STRING                    likewise for STRING.

The list or vector QPAT is a template.  The predicate formed
by a backquote-style pattern is a combination of those
formed by any sub-patterns, wrapped in a top-level condition:
EXPVAL must be \"congruent\" with the template.  For example:

  (bq-opt technical ,forum)

The predicate is the logical-AND of:
 - Is EXPVAL a list of two elements?
 - Is the first element the symbol `technical'?
 - True!  (The second element can be anything, and for the sake
   of the body forms, its value is bound to the symbol `forum'.)"
  (declare (debug (pcase-QPAT)))
  (cond
   ((eq (car-safe qpat) '\,) (cadr qpat))
   ((vectorp qpat)
    `(and (pred vectorp)
          (app length ,(length qpat))
          ,@(let ((upats nil))
              (dotimes (i (length qpat))
                (push `(app (pcase--flip aref ,i) ,(list '\` (aref qpat i)))
                      upats))
              (nreverse upats))))
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ;;; Added case (optional QPAT)
   ((pcase qpat
      (`((optional ,qpat1) . ,qpat2)
       `(or (and (pred consp)
               (app car ,qpat1)
               (app cdr ,(list 'bq-opt qpat2)))
            ,(list 'bq-opt qpat2)))))
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ((consp qpat)
    `(and (pred consp)
          (app car ,(list 'bq-opt (car qpat)))
          (app cdr ,(list 'bq-opt (cdr qpat)))))
   ((or (stringp qpat) (numberp qpat) (symbolp qpat)) `',qpat)
   ;; In all other cases just raise an error so we can't break
   ;; backward compatibility when adding \` support for other
   ;; compounded values that are not `consp'
   (t (error "Unknown QPAT: %S" qpat))))

Usage example with optional keywords in EXPVAL:

(pcase '(:def "k" foo :wk "ho")
  ((bq-opt ((optional :def)
            ,(and (or (pred stringp) (pred vectorp)) key)
            ,(and (pred symbolp) def)
            (optional :wk)
            ,(and (pred stringp) desc)
            ))
   (format "%s | %s | %s" key def desc)))

Result:

"k | foo | ho"

Usage example without optional keywords in EXPVAL:

(pcase '("k" foo "ho")
  ((bq-opt ((optional :def)
            ,(and (or (pred stringp) (pred vectorp)) key)
            ,(and (pred symbolp) def)
            (optional :wk)
            ,(and (pred stringp) desc)
            ))
   (format "%s | %s | %s" key def desc)))

Result:

"k | foo | ho"
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • Could you explain a bit the logic behind the code block you added for the solution? I'm having a difficult time understanding it, specifically the `(or ..)` form. Also, I would rather add this clause to ``--pcase-macroexpander` itself because I may want to extend the macro in the future and I don't want to create a different alternative pattern each time--I'd rather extend the existing one. I think I'd need make the clause you added not use `pcase` in that case, right? – Aquaactress Feb 17 '21 at 20:11
  • Also, this does not work for vectors. – Aquaactress Feb 21 '21 at 22:33
  • @Aquaactress Sorry, I will answer in more detail when I have some spare time. (I am really busy right now.) I thought the source code would be quite self-explanatory so I didn't go too deep into the details. As time allows I will add some more comments. The vector case does not work since this was solely an answer to the question with a proof-of-concept and not a productive solution. I have also another simple idea that should cover all cases. Maybe, I add another answer as time allows. – Tobias Feb 22 '21 at 06:49
  • I highly recommend not to touch such basic stuff like `pcase` with `advice-add`. Rather write your own version (bound to a different symbol), especially if it is as simple as in this case. Make a feature-request at `bug-gnu-emacs@gnu.org` if you think that your feature is general enough to be included into `pcase`. – Tobias Feb 22 '21 at 06:55
0

My second solution is not an attempt to modify or replace the backquote pattern of pcase
but it introduces a new pcase macro that works in collaboration with backquote patterns.

It does quite exactly what you proposed in your question:

Of course I realize it is possible to copy over the clause in my first example in all possible combinations omitting either :def, :wk or both. However, that's tedious and duplicates much code.

But, the tedious duplication of code is done by the pcase macro optional.
The doc-string of optional already describes quite good how it works.

(pcase-defmacro optional (pat-opt pat-tail)
  "Define a pattern (optional PAT-OPT PAT-TAIL) for `pcase'.
The pattern matches
- if EXPVAL is a cons with the car matching PAT-OPT
  and the cdr matching PAT-TAIL
  -- or --
- if EXPVAL matches PAT-TAIL."
  (declare (debug (pcase-PAT pcase-PAT)))
  `(or (and (app car ,pat-opt) (app cdr ,pat-tail))
       ,pat-tail))

As already mentioned, optional is a pattern keyword that you can use in conjunction with backquoted patterns.
It is not parsed by a modified backquote-pcase-macro as in my first solution.

Therefore, the pattern for your usage example looks a bit different from your proposed pattern.
Nevertheless, you do not need to write the tail behind the optional keyword twice.

(pcase val
    ((optional
      :def
      `(,(and (or (pred stringp) (pred vectorp)) key)
        ,(and (pred symbolp) def)
        .
        ,(optional
          :wk
          `(,(and (pred stringp) desc)))))
     (format "%s | %s | %s" key def desc)))

The evaluation of this example gives:

"k | foo | ho"
Tobias
  • 32,569
  • 1
  • 34
  • 75