Subject: bug#49407: Request: Specify default values in `map-let` in Map.el

From: Okam <[email-masked]>
Date: Sat, 22 Jul 2023 18:46:12 +0000

On 2023-07-22 01:48 UTC, Michael Heerdegen wrote:
> 
> | diff --git a/lisp/emacs-lisp/map.el b/lisp/emacs-lisp/map.el
> | index 7a48ba47434..7f127f312de 100644
> | --- a/lisp/emacs-lisp/map.el
> | +++ b/lisp/emacs-lisp/map.el
> | @@ -50,18 +50,23 @@ map
> |
> |  ARGS is a list of elements to be matched in the map.
> |
> | -Each element of ARGS can be of the form (KEY PAT), in which case KEY is
> | -evaluated and searched for in the map.  The match fails if for any KEY
> | -found in the map, the corresponding PAT doesn't match the value
> | -associated with the KEY.
> | +Each element of ARGS can be of the form (KEY PAT) or (KEY PAT DEFAULT),
> | +
> | +in which case KEY is evaluated and searched for in the map and
> 
> That line break in the last sentence is somehow... broken.
> 
> | +DEFAULT is the evaluated value used if KEY is not found.  The
> | +match fails if for any KEY found in the map, the corresponding
> | +PAT doesn't match the value associated with the KEY.  The match
> | +fails if for any KEY not found in the map, the corresponding PAT
> | +doesn't match the DEFAULT value, if given.
> 
> Here is something grammatically not right in the last sentence.  And I
> think we should say that that last case (PAT doesn't match DEFAULT) only
> lets matching fail when the KEY is not found.
> 
> 
> | -Keys in ARGS not found in the map are ignored, and the match doesn't
> | -fail."
> | +When no DEFAULT value is given, the default value for keys in
> | +ARGS not found in the map is nil, and the match doesn't fail so
> | +long at PAT, if given, matches nil."
> |    `(and (pred mapp)
> |          ,@(map--make-pcase-bindings args)))
> 
> Typo in doc ("at").  And can't we simply say that omitting DEFAULT is
> equivalent to a DEFAULT of nil?
> 
> | @@ -599,7 +606,11 @@ map--make-pcase-bindings
> |    "Return a list of pcase bindings from ARGS to the elements of a map."
> |    (mapcar (lambda (elt)
> |              (cond ((consp elt)
> | -                   `(app (pcase--flip map-elt ,(car elt)) ,(cadr elt)))
> | +                   (if (cddr elt)
> | +                       `(app (lambda (arg)
> | +                               (map-elt arg ,(car elt) ,(caddr elt)))
> | +                             ,(cadr elt))
> | +                     `(app (pcase--flip map-elt ,(car elt)) ,(cadr elt))))
> 
> If we don't extend `pcase--flip' to support this case, I think your
> lambda would need to use an uninterned symbol as parameter to avoid
> collisions with the involved expressions.
> 
> 
> Michael.

Please see the attached file. I have re-worded the doc string and added 
a wrapper macro to re-order the arguments to `map-elt` for the `app` 
pattern.
From 6cdf145fbc81c75c88c8f5e98450a30688d24901 Mon Sep 17 00:00:00 2001
From: Earl Hyatt <[email-masked]>
Date: Thu, 20 Jul 2023 21:44:41 -0400
Subject: [PATCH v2] Allow default values in 'map-let' and the pcase 'map' form

* lisp/emacs-lisp/map.el (map-let, map)
(map--make-pcase-bindings): Add a third argument for specifying a
default value, like in 'map-elt'. (Bug#49407)

* lisp/emacs-lisp/map.el (map--make-pcase-bindings): Clarify that keys
that aren't found aren't ignored, they actually get the value
nil (unless the new default value is given). The overall pattern can
still fail to match if the sub-pattern for the unfound key doesn't
match nil.

* test/lisp/emacs-lisp/map-tests.el (test-map-let-default)
(test-map-plist-pcase-default, test-map-pcase-matches): Add tests,
including for the above item.
---
 lisp/emacs-lisp/map.el            | 40 ++++++++++++++++++------
 test/lisp/emacs-lisp/map-tests.el | 52 +++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 10 deletions(-)

diff --git a/lisp/emacs-lisp/map.el b/lisp/emacs-lisp/map.el
index 7a48ba47434..35b8998679a 100644
--- a/lisp/emacs-lisp/map.el
+++ b/lisp/emacs-lisp/map.el
@@ -50,18 +50,26 @@ map
 
 ARGS is a list of elements to be matched in the map.
 
-Each element of ARGS can be of the form (KEY PAT), in which case KEY is
-evaluated and searched for in the map.  The match fails if for any KEY
-found in the map, the corresponding PAT doesn't match the value
-associated with the KEY.
+Each element in ARGS can be of the form (KEY PAT) or (KEY PAT
+DEFAULT), where KEY is the key sought in the map, PAT is a
+`pcase' pattern, and DEFAULT is the default value to use if KEY
+isn't found.  Both KEY and DEFAULT are evaluated.  If DEFAULT
+isn't given, then nil is used.
 
 Each element can also be a SYMBOL, which is an abbreviation of
 a (KEY PAT) tuple of the form (\\='SYMBOL SYMBOL).  When SYMBOL
 is a keyword, it is an abbreviation of the form (:SYMBOL SYMBOL),
 useful for binding plist values.
 
-Keys in ARGS not found in the map are ignored, and the match doesn't
-fail."
+The match fails if:
+
+- KEY is found and PAT does not match the associated value
+
+- KEY is not found, DEFAULT is given, and PAT does not match
+  DEFAULT
+
+- KEY is not found, DEFAULT is not given, and PAT does not match
+  nil"
   `(and (pred mapp)
         ,@(map--make-pcase-bindings args)))
 
@@ -71,12 +79,14 @@ map-let
 KEYS can be a list of symbols, in which case each element will be
 bound to the looked up value in MAP.
 
-KEYS can also be a list of (KEY VARNAME) pairs, in which case
-KEY is an unquoted form.
+KEYS can also be a list of (KEY VARNAME) pairs and
+\(KEY VARNAME DEFAULT) triples, in which case KEY is an
+unquoted form.
 
 MAP can be an alist, plist, hash-table, or array."
   (declare (indent 2)
-           (debug ((&rest &or symbolp ([form symbolp])) form body)))
+           (debug ((&rest &or symbolp ([form symbolp &optional form]))
+                   form body)))
   `(pcase-let ((,(map--make-pcase-patterns keys) ,map))
      ,@body))
 
@@ -595,11 +605,21 @@ map-into
     (map-into \\='((1 . 3)) \\='(hash-table :test eql))"
   (map--into-hash map (cdr type)))
 
+(defmacro map--pcase-map-elt (key default map)
+  "A macro to make MAP the last argument to `map-elt'.
+
+This allows using default values for `map-elt', which can't be
+done using `pcase--flip'.
+
+KEY is the key sought in the map.  DEFAULT is the default value."
+  `(map-elt ,map ,key ,default))
+
 (defun map--make-pcase-bindings (args)
   "Return a list of pcase bindings from ARGS to the elements of a map."
   (mapcar (lambda (elt)
             (cond ((consp elt)
-                   `(app (pcase--flip map-elt ,(car elt)) ,(cadr elt)))
+                   `(app (map--pcase-map-elt ,(car elt) ,(caddr elt))
+                         ,(cadr elt)))
                   ((keywordp elt)
                    (let ((var (intern (substring (symbol-name elt) 1))))
                      `(app (pcase--flip map-elt ,elt) ,var)))
diff --git a/test/lisp/emacs-lisp/map-tests.el b/test/lisp/emacs-lisp/map-tests.el
index 86c0e9e0503..42458fbfeb7 100644
--- a/test/lisp/emacs-lisp/map-tests.el
+++ b/test/lisp/emacs-lisp/map-tests.el
@@ -577,6 +577,13 @@ test-map-let
     (should (= b 2))
     (should-not c)))
 
+(ert-deftest test-map-let-default ()
+  (map-let (('foo a 3)
+            ('baz b 4))
+      '((foo . 1))
+    (should (= a 1))
+    (should (= b 4))))
+
 (ert-deftest test-map-merge ()
   "Test `map-merge'."
   (should (equal (sort (map-merge 'list '(a 1) '((b . 2) (c . 3))
@@ -617,6 +624,51 @@ test-map-plist-pcase
                      (list one two))
                    '(1 2)))))
 
+(ert-deftest test-map-plist-pcase-default ()
+  (let ((plist '(:two 2)))
+    (should (equal (pcase-let (((map (:two two 33)
+                                     (:three three 44))
+                                plist))
+                     (list two three))
+                   '(2 44)))))
+
+(ert-deftest test-map-pcase-matches ()
+  (let ((plist '(:two 2)))
+    (should (equal (pcase plist
+                     ((map (:two two 33)
+                           (:three three))
+                      (list two three))
+                     (_ 'fail))
+                   '(2 nil)))
+
+    (should (equal (pcase plist
+                     ((map (:two two 33)
+                           (:three three 44))
+                      (list two three))
+                     (_ 'fail))
+                   '(2 44)))
+
+    (should (equal (pcase plist
+                     ((map (:two two 33)
+                           (:three `(,a . ,b) '(11 . 22)))
+                      (list two a b))
+                     (_ 'fail))
+                   '(2 11 22)))
+
+    (should (equal 'fail
+                   (pcase plist
+                     ((map (:two two 33)
+                           (:three `(,a . ,b) 44))
+                      (list two a b))
+                     (_ 'fail))))
+
+    (should (equal 'fail
+                   (pcase plist
+                     ((map (:two two 33)
+                           (:three `(,a . ,b) nil))
+                      (list two a b))
+                     (_ 'fail))))))
+
 (ert-deftest test-map-setf-alist-insert-key ()
   (let ((alist))
     (should (equal (setf (map-elt alist 'key) 'value)
-- 
2.34.1