;;; map.el --- Map manipulation functions -*- lexical-binding: t; -*-
-;; Copyright (C) 2015 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2016 Free Software Foundation, Inc.
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: convenience, map, hash-table, alist, array
-;; Version: 1.0
+;; Version: 1.1
;; Package: map
;; Maintainer: emacs-devel@gnu.org
;;; Code:
(require 'seq)
+(eval-when-compile (require 'cl-lib))
(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 the corresponding element of the map.
+ "Build a `pcase' pattern matching map elements.
-ARGS can be a list elements of the form (KEY . PAT) or elements
-of the form SYMBOL, which stands for (SYMBOL . SYMBOL)."
- `(and (pred map-p)
+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 to the KEY.
+
+Each element can also be a SYMBOL, which is an abbreviation of a (KEY
+PAT) tuple of the form (\\='SYMBOL SYMBOL).
+
+Keys in ARGS not found in the map are ignored, and the match doesn't
+fail."
+ `(and (pred mapp)
,@(map--make-pcase-bindings args)))
-(defmacro map-let (args map &rest body)
- "Bind the variables in ARGS to the elements of MAP then evaluate BODY.
+(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.
-ARGS can be an alist of key/binding pairs or a list of keys. MAP
-can be a list, hash-table or array."
+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 args) ,map))
+ `(pcase-let ((,(map--make-pcase-patterns keys) ,map))
,@body))
-(defmacro map--dispatch (spec &rest args)
- "Evaluate one of the provided forms depending on the type of MAP.
-
-SPEC can be a map or a list of the form (VAR MAP [RESULT]).
-ARGS should have the form [TYPE FORM]...
+(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'.
+`: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.
-
-\(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)))
+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.
+ "Lookup KEY in MAP 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 (map--elt-list map key 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.
+ "Associate KEY with VALUE in MAP and return VALUE.
If KEY is already present in MAP, replace the associated value
with VALUE.
MAP can be a list, hash-table or array."
- (declare (debug t))
- (let ((symbol (symbolp map)))
- `(progn
- (map--dispatch (m ,map m)
- :list (if ,symbol
- (setq ,map (cons (cons ,key ,value) m))
- (error "Literal lists are not allowed, %s must be a symbol" ',map))
- :hash-table (puthash ,key ,value m)
- :array (aset m ,key ,value)))))
-
-(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.
+ `(setf (map-elt ,map ,key) ,value))
+
+(defun map-delete (map key)
+ "Delete KEY from MAP and return MAP.
+No error is signaled if KEY is not a key of MAP. If MAP is an
+array, store nil at the index KEY.
MAP can be a list, hash-table or array."
- (declare (debug t))
- (let ((symbol (symbolp map)))
- `(progn
- (map--dispatch (m ,map m)
- :list (if ,symbol
- (setq ,map (map--delete-alist m ,key))
- (error "Literal lists are not allowed, %s must be a symbol" ',map))
- :hash-table (remhash ,key m)
- :array (map--delete-array m ,key)))))
+ (map--dispatch map
+ :list (setf (alist-get key map nil t) nil)
+ :hash-table (remhash key map)
+ :array (and (>= key 0)
+ (<= key (seq-length map))
+ (aset map key nil)))
+ map)
(defun map-nested-elt (map keys &optional default)
"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)
+ (when (mapp acc)
(map-elt acc key)))
keys
map)
function
map))
+(defun map-do (function map)
+ "Apply FUNCTION to each element of MAP and return nil.
+FUNCTION.is called with two arguments, the key and the value."
+ (funcall (map--dispatch map
+ :list #'map--do-alist
+ :hash-table #'maphash
+ :array #'map--do-array)
+ function
+ map))
+
(defun map-keys-apply (function map)
"Return the result of applying FUNCTION to each key of MAP.
map))
(defun map-filter (pred map)
- "Return an alist of the key/val pairs for 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)
(map-filter (lambda (key val) (not (funcall pred key val)))
map))
-(defun map-p (map)
+(defun mapp (map)
"Return non-nil if MAP is a map (list, hash-table or array)."
(or (listp map)
(hash-table-p map)
(arrayp map)))
(defun map-empty-p (map)
- "Return non-nil is MAP is empty.
+ "Return non-nil if MAP is empty.
MAP can be a list, hash-table or array."
(map--dispatch map
:array (seq-empty-p map)
:hash-table (zerop (hash-table-count map))))
-(defun map-contains-key-p (map key &optional testfn)
- "Return non-nil if MAP contain the key KEY, nil otherwise.
+(defun map-contains-key (map key &optional testfn)
+ "Return non-nil if MAP contain 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-p (pred map)
- "Return a key/value pair for which (PRED key val) is non-nil in MAP.
+(defun map-some (pred map)
+ "Return a non-nil if (PRED key val) is non-nil for any key/value pair in MAP.
MAP can be a list, hash-table or array."
(catch 'map--break
(map-apply (lambda (key value)
- (when (funcall pred key value)
- (throw 'map--break (cons key value))))
+ (let ((result (funcall pred key value)))
+ (when result
+ (throw 'map--break result))))
map)
nil))
MAP can be a list, hash-table or array."
(catch 'map--break
(map-apply (lambda (key value)
- (or (funcall pred key value)
- (throw 'map--break nil)))
- map)
+ (or (funcall pred key value)
+ (throw 'map--break nil)))
+ map)
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 MAPS.
MAP can be a list, hash-table or array."
- (let (result)
+ (let ((result (map-into (pop maps) type)))
(while maps
+ ;; FIXME: When `type' is `list', we get an O(N^2) behavior.
+ ;; For small tables, this is fine, but for large tables, we
+ ;; should probably use a hash-table internally which we convert
+ ;; to an alist in the end.
(map-apply (lambda (key value)
- (map-put result key value))
+ (setf (map-elt result key) value))
(pop maps)))
- (map-into result type)))
+ result))
+
+(defun map-merge-with (type function &rest maps)
+ "Merge into a map of type TYPE all the key/value pairs in MAPS.
+When two maps contain the same key, call FUNCTION on the two
+values and use the value returned by it.
+MAP can be a list, hash-table or array."
+ (let ((result (map-into (pop maps) type))
+ (not-found (cons nil nil)))
+ (while maps
+ (map-apply (lambda (key value)
+ (cl-callf (lambda (old)
+ (if (eq old not-found)
+ value
+ (funcall function old value)))
+ (map-elt result key not-found)))
+ (pop maps)))
+ result))
(defun map-into (map type)
"Convert the map MAP into a map of type TYPE.
(pcase type
(`list (map-pairs map))
(`hash-table (map--into-hash-table map))
- (t (error "Not a map type name: %S" type))))
+ (_ (error "Not a map type name: %S" type))))
+
+(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."
(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-list (map key &optional default)
- "Lookup, in the list MAP, the value associated with KEY and return it.
-If KEY is not found, return DEFAULT which defaults to nil."
- (let ((pair (assoc key map)))
- (if pair
- (cdr pair)
- default)))
-
-(defun map--elt-array (map key &optional default)
- "Return the element of the array MAP at the index KEY.
-If KEY is not found, return DEFAULT which defaults to 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))
-
-(defun map--delete-array (map key)
- "Set nil in the array MAP at the index KEY if present and return MAP."
- (let ((len (seq-length map)))
- (and (>= key 0)
- (<= key len)
- (aset map key nil)))
- map)
+ (let ((index 0))
+ (seq-map (lambda (elt)
+ (prog1
+ (funcall function index elt)
+ (setq index (1+ index))))
+ map)))
+
+(defun map--do-alist (function alist)
+ "Private function used to iterate over ALIST using FUNCTION."
+ (seq-do (lambda (pair)
+ (funcall function
+ (car pair)
+ (cdr pair)))
+ alist))
+
+(defun map--do-array (function array)
+ "Private function usde to iterate over ARRAY using FUNCTION."
+ (seq-do-indexed (lambda (elt index)
+ (funcall function index elt))
+ array))
(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))
"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)) ,(cdr elt))
+ `(app (pcase--flip map-elt ,(car elt)) ,(cadr elt))
`(app (pcase--flip map-elt ',elt) ,elt)))
args))