]> code.delx.au - gnu-emacs/blobdiff - lisp/emacs-lisp/map.el
Rename map-contains-key-p and map-some-p
[gnu-emacs] / lisp / emacs-lisp / map.el
index 621c37f2b76cec38395679547aff946a3062e82c..4e7d3b91b16019fa41e090ae440af73d247895f4 100644 (file)
 
 (require 'seq)
 
+(pcase-defmacro map (&rest args)
+  "pcase pattern matching map elements.
+Matches if the object is a map (list, hash-table or array), and
+binds values from ARGS to their corresponding elements of the map.
+
+ARGS can be a list elements of the form (KEY PAT), in which case
+KEY in an unquoted form.
+
+ARGS can also be a list of symbols, which stands for ('SYMBOL
+SYMBOL)."
+  `(and (pred map-p)
+        ,@(map--make-pcase-bindings args)))
+
+(defmacro map-let (keys map &rest body)
+  "Bind the variables in KEYS to the elements of MAP then evaluate BODY.
+
+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.
+
+MAP can be a list, hash-table or array."
+  (declare (indent 2) (debug t))
+  `(pcase-let ((,(map--make-pcase-patterns keys) ,map))
+     ,@body))
+
+(eval-when-compile
+  (defmacro map--dispatch (map-var &rest args)
+    "Evaluate one of the forms specified by ARGS based on the type of MAP.
+
+The following keyword types are meaningful: `:list',
+`:hash-table' and `:array'.
+
+An error is thrown if MAP is neither a list, hash-table nor array.
+
+Return RESULT if non-nil or the result of evaluation of the form."
+    (declare (debug t) (indent 1))
+    `(cond ((listp ,map-var) ,(plist-get args :list))
+           ((hash-table-p ,map-var) ,(plist-get args :hash-table))
+           ((arrayp ,map-var) ,(plist-get args :array))
+           (t (error "Unsupported map: %s" ,map-var)))))
+
 (defun map-elt (map key &optional default)
   "Perform a lookup in MAP of KEY and return its associated value.
 If KEY is not found, return DEFAULT which defaults to nil.
 
-If MAP is a list, `equal' is used to lookup KEY."
+If MAP is a list, `eql' is used to lookup KEY.
+
+MAP can be a list, hash-table or array."
+  (declare
+   (gv-expander
+    (lambda (do)
+      (gv-letplace (mgetter msetter) `(gv-delay-error ,map)
+        (macroexp-let2* nil
+            ;; Eval them once and for all in the right order.
+            ((key key) (default default))
+          `(if (listp ,mgetter)
+               ;; Special case the alist case, since it can't be handled by the
+               ;; map--put function.
+               ,(gv-get `(alist-get ,key (gv-synthetic-place
+                                          ,mgetter ,msetter)
+                                    ,default)
+                        do)
+             ,(funcall do `(map-elt ,mgetter ,key ,default)
+                       (lambda (v) `(map--put ,mgetter ,key ,v)))))))))
   (map--dispatch map
-    :list (or (cdr (assoc key map)) default)
+    :list (alist-get key map default)
     :hash-table (gethash key map default)
-    :array (map--elt-array map key default)))
+    :array (if (and (>= key 0) (< key (seq-length map)))
+               (seq-elt map key)
+             default)))
 
 (defmacro map-put (map key value)
   "In MAP, associate KEY with VALUE and return MAP.
 If KEY is already present in MAP, replace the associated value
-with VALUE."
-  (declare (debug t))
-  `(progn
-     (map--dispatch (m ,map m)
-       :list (setq ,map (cons (cons ,key ,value) m))
-       :hash-table (puthash ,key ,value m)
-       :array (aset m ,key ,value))))
+with VALUE.
+
+MAP can be a list, hash-table or array."
+  (macroexp-let2 nil map map
+    `(progn
+       (setf (map-elt ,map ,key) ,value)
+       ,map)))
 
 (defmacro map-delete (map key)
   "In MAP, delete the key KEY if present and return MAP.
-If MAP is an array, store nil at the index KEY."
+If MAP is an array, store nil at the index KEY.
+
+MAP can be a list, hash-table or array."
   (declare (debug t))
-  `(progn
-     (map--dispatch (m ,map m)
-       :list (setq ,map (map--delete-alist m ,key))
-       :hash-table (remhash ,key m)
-       :array (aset m ,key nil))))
+  (gv-letplace (mgetter msetter) `(gv-delay-error ,map)
+    (macroexp-let2 nil key key
+      `(if (not (listp ,mgetter))
+           (map--delete ,mgetter ,key)
+         ;; The alist case is special, since it can't be handled by the
+         ;; map--delete function.
+         (setf (alist-get ,key (gv-synthetic-place ,mgetter ,msetter)
+                          nil t)
+               nil)
+         ,mgetter))))
 
 (defun map-nested-elt (map keys &optional default)
-  "Travserse MAP using KEYS and return the looked up value or DEFAULT if nil.
+  "Traverse MAP using KEYS and return the looked up value or DEFAULT if nil.
+
 Map can be a nested map composed of alists, hash-tables and arrays."
   (or (seq-reduce (lambda (acc key)
                     (when (map-p acc)
@@ -86,33 +157,43 @@ Map can be a nested map composed of alists, hash-tables and arrays."
       default))
 
 (defun map-keys (map)
-  "Return the list of keys in MAP."
+  "Return the list of keys in MAP.
+
+MAP can be a list, hash-table or array."
   (map-apply (lambda (key _) key) map))
 
 (defun map-values (map)
-  "Return the list of values in MAP."
+  "Return the list of values in MAP.
+
+MAP can be a list, hash-table or array."
   (map-apply (lambda (_ value) value) map))
 
 (defun map-pairs (map)
-  "Return the elements of MAP as key/value association lists."
-  (map-apply (lambda (key value)
-               (cons key value))
-             map))
+  "Return the elements of MAP as key/value association lists.
+
+MAP can be a list, hash-table or array."
+  (map-apply #'cons map))
 
 (defun map-length (map)
-  "Return the length of MAP."
+  "Return the length of MAP.
+
+MAP can be a list, hash-table or array."
   (length (map-keys map)))
 
 (defun map-copy (map)
-  "Return a copy of MAP."
+  "Return a copy of MAP.
+
+MAP can be a list, hash-table or array."
   (map--dispatch map
     :list (seq-copy map)
     :hash-table (copy-hash-table map)
     :array (seq-copy map)))
 
 (defun map-apply (function map)
-  "Return the result of applying FUNCTION to each element of MAP.
-FUNCTION is called with two arguments, the key and the value."
+  "Apply FUNCTION to each element of MAP and return the result as a list.
+FUNCTION is called with two arguments, the key and the value.
+
+MAP can be a list, hash-table or array."
   (funcall (map--dispatch map
              :list #'map--apply-alist
              :hash-table #'map--apply-hash-table
@@ -121,19 +202,25 @@ FUNCTION is called with two arguments, the key and the value."
            map))
 
 (defun map-keys-apply (function map)
-  "Return the result of applying FUNCTION to each key of MAP."
+  "Return the result of applying FUNCTION to each key of MAP.
+
+MAP can be a list, hash-table or array."
   (map-apply (lambda (key _)
                (funcall function key))
              map))
 
 (defun map-values-apply (function map)
-  "Return the result of applying FUNCTION to each value of MAP."
+  "Return the result of applying FUNCTION to each value of MAP.
+
+MAP can be a list, hash-table or array."
   (map-apply (lambda (_ val)
                (funcall function val))
              map))
 
 (defun map-filter (pred map)
-  "Return an alist of the key/val pairs of which (PRED key val) is non-nil in MAP."
+  "Return an alist of key/val pairs for which (PRED key val) is non-nil in MAP.
+
+MAP can be a list, hash-table or array."
   (delq nil (map-apply (lambda (key val)
                          (if (funcall pred key val)
                              (cons key val)
@@ -141,7 +228,9 @@ FUNCTION is called with two arguments, the key and the value."
                        map)))
 
 (defun map-remove (pred map)
-  "Return an alist of the key/val pairs of which (PRED key val) is nil in MAP."
+  "Return an alist of the key/val pairs for which (PRED key val) is nil in MAP.
+
+MAP can be a list, hash-table or array."
   (map-filter (lambda (key val) (not (funcall pred key val)))
               map))
 
@@ -153,17 +242,24 @@ FUNCTION is called with two arguments, the key and the value."
 
 (defun map-empty-p (map)
   "Return non-nil is MAP is empty.
+
 MAP can be a list, hash-table or array."
-  (null (map-keys map)))
+  (map--dispatch map
+    :list (null map)
+    :array (seq-empty-p map)
+    :hash-table (zerop (hash-table-count map))))
 
-(defun map-contains-key-p (map key &optional testfn)
+(defun map-contains-key (map key &optional testfn)
   "Return non-nil if MAP contain the key KEY, nil otherwise.
 Equality is defined by TESTFN if non-nil or by `equal' if nil.
+
 MAP can be a list, hash-table or array."
-  (seq-contains-p (map-keys map) key testfn))
+  (seq-contains (map-keys map) key testfn))
+
+(defun map-some (pred map)
+  "Return a non-nil if (PRED key val) is non-nil for any key/value pair in MAP.
 
-(defun map-some-p (pred map)
-  "Return any key/value pair for which (PRED key val) is non-nil is MAP."
+MAP can be a list, hash-table or array."
   (catch 'map--break
     (map-apply (lambda (key value)
                  (when (funcall pred key value)
@@ -172,7 +268,9 @@ MAP can be a list, hash-table or array."
     nil))
 
 (defun map-every-p (pred map)
-  "Return non-nil if (PRED key val) is non-nil for all elements of the map MAP."
+  "Return non-nil if (PRED key val) is non-nil for all elements of the map MAP.
+
+MAP can be a list, hash-table or array."
   (catch 'map--break
     (map-apply (lambda (key value)
                  (or (funcall pred key value)
@@ -181,52 +279,33 @@ MAP can be a list, hash-table or array."
     t))
 
 (defun map-merge (type &rest maps)
-  "Merge into a map of type TYPE all the key/value pairs in the maps MAPS."
+  "Merge into a map of type TYPE all the key/value pairs in the maps MAPS.
+
+MAP can be a list, hash-table or array."
   (let (result)
     (while maps
       (map-apply (lambda (key value)
-                   (map-put result key value))
+                   (setf (map-elt result key) value))
                  (pop maps)))
     (map-into result type)))
 
 (defun map-into (map type)
   "Convert the map MAP into a map of type TYPE.
-TYPE can be one of the following symbols: list or hash-table."
+
+TYPE can be one of the following symbols: list or hash-table.
+MAP can be a list, hash-table or array."
   (pcase type
     (`list (map-pairs map))
     (`hash-table (map--into-hash-table map))
-    (t (error "Not a map type name: %S" type))))
-
-(defmacro map--dispatch (spec &rest args)
-  "Evaluate one of the provided forms depending on the type of MAP.
+    (_ (error "Not a map type name: %S" type))))
 
-SPEC can be a map or a list of the form (VAR MAP [RESULT]).
-ARGS should have the form [TYPE FORM]...
-
-The following keyword types are meaningful: `:list',
-`:hash-table' and `array'.
-
-An error is thrown if MAP is neither a list, hash-table or array.
-
-Return RESULT if non-nil or the result of evaluation of the
-form.
-
-\(fn (VAR MAP [RESULT]) &rest ARGS)"
-  (declare (debug t) (indent 1))
-  (unless (listp spec)
-    (setq spec `(,spec ,spec)))
-  (let ((map-var (car spec))
-        (result-var (make-symbol "result")))
-    `(let ((,map-var ,(cadr spec))
-           ,result-var)
-       (setq ,result-var
-             (cond ((listp ,map-var) ,(plist-get args :list))
-                   ((hash-table-p ,map-var) ,(plist-get args :hash-table))
-                   ((arrayp ,map-var) ,(plist-get args :array))
-                   (t (error "Unsupported map: %s" ,map-var))))
-       ,@(when (cddr spec)
-          `((setq ,result-var ,@(cddr spec))))
-       ,result-var)))
+(defun map--put (map key v)
+  (map--dispatch map
+    :list (let ((p (assoc key map)))
+            (if p (setcdr p v)
+              (error "No place to change the mapping for %S" key)))
+    :hash-table (puthash key v map)
+    :array (aset map key v)))
 
 (defun map--apply-alist (function map)
   "Private function used to apply FUNCTION over MAP, MAP being an alist."
@@ -236,6 +315,15 @@ form.
                       (cdr pair)))
            map))
 
+(defun map--delete (map key)
+  (map--dispatch map
+    :list (error "No place to remove the mapping for %S" key)
+    :hash-table (remhash key map)
+    :array (and (>= key 0)
+                (<= key (seq-length map))
+                (aset map key nil)))
+  map)
+
 (defun map--apply-hash-table (function map)
   "Private function used to apply FUNCTION over MAP, MAP being a hash-table."
   (let (result)
@@ -246,36 +334,38 @@ form.
 
 (defun map--apply-array (function map)
   "Private function used to apply FUNCTION over MAP, MAP being an array."
-   (let ((index 0))
-     (seq-map (lambda (elt)
-                (prog1
-                    (funcall function index elt)
-                  (setq index (1+ index))))
-              map)))
-
-(defun map--elt-array (map key &optional default)
-  "Return the element of the arary MAP at the index KEY, or DEFAULT if nil."
-  (let ((len (seq-length map)))
-    (or (and (>= key 0)
-             (<= key len)
-             (seq-elt map key))
-        default)))
-
-
-(defun map--delete-alist (map key)
-  "Return MAP with KEY removed."
-  (seq-remove (lambda (pair)
-                (equal key (car pair)))
-              map))
+  (let ((index 0))
+    (seq-map (lambda (elt)
+               (prog1
+                   (funcall function index elt)
+                 (setq index (1+ index))))
+             map)))
 
 (defun map--into-hash-table (map)
   "Convert MAP into a hash-table."
   (let ((ht (make-hash-table :size (map-length map)
                              :test 'equal)))
     (map-apply (lambda (key value)
-                 (map-put ht key value))
+                 (setf (map-elt ht key) value))
                map)
     ht))
 
+(defun map--make-pcase-bindings (args)
+  "Return a list of pcase bindings from ARGS to the elements of a map."
+  (seq-map (lambda (elt)
+             (if (consp elt)
+                 `(app (pcase--flip map-elt ,(car elt)) ,(cadr elt))
+               `(app (pcase--flip map-elt ',elt) ,elt)))
+           args))
+
+(defun map--make-pcase-patterns (args)
+  "Return a list of `(map ...)' pcase patterns built from ARGS."
+  (cons 'map
+        (seq-map (lambda (elt)
+                   (if (and (consp elt) (eq 'map (car elt)))
+                       (map--make-pcase-patterns elt)
+                     elt))
+                 args)))
+
 (provide 'map)
 ;;; map.el ends here