The Case of PCase
Recently I have had to take a look into using pcase for pattern
matching in elisp. To be quite frank, I had no idea there even was
pattern matching support in elisp and always used cond
, which is
more like a simple switch statement. The more I got into pcase, the
more I saw what a powerful tool it was and how much it could simplify
a lot of the code I was writing for my emacs packages.
Case Study
Let's assume I have a list of property lists, and I know I should always expect the first plist to contain a property with a certain name, in which case I will execute my logic. This will be our example plist:
(setq ovs '((:size 6 :color "red") (:name "foo" :prop "bar")))
:size | 6 | :color | red |
:name | foo | :prop | bar |
Now, let's assume I want to check whether the "size" property is present in the first item, in which case I will print out its value:
(let ((val (plist-get (car ovs) :size)))
(if val
(message "Here is the value %s" val)
(message "No size property found")))
Here is the value 6
Nice, but we are making some assumptions here, first of all, we
assume we always get a plist as an argument. Then we are creating this
val
variable so we won't repeat the same function calls later for
our message.
One way we could check these assumptions is the following, starting from making sure that what we are passing is indeed a list:
(let ((ovs '((:size 6 :color "red") (:name "foo" :prop "bar"))))
(pcase ovs
((`(,a ,b)
(message "Size is %s" (plist-get a :size))))
(_ (message "anything else"))))
Size is 6
So, what did we just do? One cool thing that you can do with pcase is deconstruction, we can actually do 2 things at the same time, first is making sure what is being passed is a list, the second is that it contains two obects, and we can even call the first one without having to use car. However, we could also consider cases where more than 2 items are present in the plist:
(let ((ovs '((:size 6 :color "red") (:name "foo" :prop "bar"))))
(pcase ovs
(`(,a . ,_)
(message "Size is %s" (plist-get a :size)))
(_ (message "anything else"))))
Size is 6
In this case we want to match a list that has at least one item, we do
not care for the other items, so we bind them to underscore _, like in
python for unused variables. However, we may want to explicitly first
check that the first item has a :size
key, in oder to do that, we
can use the guard
function inside an and clause, this will ensure
that not only what we are passing is a list of 1 or more items, but
also that that first item has a :size key:
(let ((ovs '((:size 6 :color "red") (:name "foo" :prop "bar"))))
(pcase ovs
((and `(,a . ,_)
(guard (plist-member a :size)))
(message "Size is %s" (plist-get a :size)))
(_ (message "anything else"))))
Size is 6
Right now, however, we handle all other cases just the same, but maybe
we want to handle cases where the object passed is not a list in a
particular way. Let's handle the case of passing an integer instead by
adding a new pattern match and using the pred
function.
(let ((ovs 5));'((:size 6 :color "red") (:name "foo" :prop "bar"))))
(pcase ovs
((and `(,a . ,_)
(guard (plist-member a :size)))
(message "Size is %s" (plist-get a :size)))
((and (pred (integerp)))
(message "You did not pass a list but an integer!"))
(_ (message "anything else"))))
You did not pass a list but an integer!
We can also use nested pcases, may seem excessive, but it can turn out
handy in some cases. In the following, we use (pred (not function))
to match anything that is not an integer, then we have a nested pcase
that will handle integer cases by binding the value of what we passed to
the variable somenumber
.
(let ((ovs 3));'((:size 6 :color "red") (:name "foo" :prop "bar"))))
(pcase ovs
((and `(,a . ,_)
(guard (plist-member a :size)))
(message "Size is %s" (plist-get a :size)))
((and (pred (not integerp)))
(message "You did not pass a list nor an integer!"))
((and (pred (integerp)) somenumber)
(pcase somenumber
(5 (message "you passed 5!"))
(_ (message "You passed an integer that is not 5"))))))
You passed an integer that is not 5
Final comments
I have gone over and, pred and guard as functions that can be used
with pcase, but there are also or
, let
(to bind the value to a
variable), cl-type
, app
and rx
(to match using regex). These are
explained in the docs
( https://www.gnu.org/software/emacs/manual/html_node/elisp/pcase-Macro.html
).
It was a pretty interesting journey this one, while it can be a little challenging or unusual as an approach to pattern matching, the capabilities and usefulness of pcase in emacs lisp make it a valuable tool that can make code more concise and readable.