diff options
| author | Stefan Monnier | 2018-12-17 14:51:01 -0500 |
|---|---|---|
| committer | Stefan Monnier | 2018-12-17 14:51:01 -0500 |
| commit | 55838e4e6a176317367c6759e0520395e80c856f (patch) | |
| tree | 49f0293f48e1084e076a936e8b3476f35efe7cf5 | |
| parent | 2c3f7f9c45985c36fd9e86c334b49b10e8c8c270 (diff) | |
| download | emacs-55838e4e6a176317367c6759e0520395e80c856f.tar.gz emacs-55838e4e6a176317367c6759e0520395e80c856f.zip | |
* lisp/emacs-lisp/map.el: Avoid special casing lists.
(map-not-inplace, map-inplace): New errors.
(map-insert): New generic function.
(map-put!): Signal map-not-inplace rather than a generic 'error'.
(map-elt): Use map-not-inplace and map-insert to avoid hardcoding
a special case for lists.
* test/lisp/emacs-lisp/map-tests.el (test-map-put!): Rename from
test-map-put. Also test the errors signaled.
| -rw-r--r-- | etc/NEWS | 3 | ||||
| -rw-r--r-- | lisp/emacs-lisp/map.el | 51 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/map-tests.el | 20 |
3 files changed, 54 insertions, 20 deletions
| @@ -307,8 +307,9 @@ the node "(emacs) Directory Variables" of the user manual. | |||
| 307 | ** map.el | 307 | ** map.el |
| 308 | *** Now defined via generic functions that can be extended via cl-defmethod. | 308 | *** Now defined via generic functions that can be extended via cl-defmethod. |
| 309 | *** Deprecate the 'map-put' macro in favor of a new 'map-put!' function. | 309 | *** Deprecate the 'map-put' macro in favor of a new 'map-put!' function. |
| 310 | *** map-contains-key now returns a boolean rather than the key. | 310 | *** 'map-contains-key' now returns a boolean rather than the key. |
| 311 | *** Deprecate the 'testfn' args of 'map-elt' and 'map-contains-key'. | 311 | *** Deprecate the 'testfn' args of 'map-elt' and 'map-contains-key'. |
| 312 | *** New generic function 'map-insert'. | ||
| 312 | 313 | ||
| 313 | --- | 314 | --- |
| 314 | ** Follow mode | 315 | ** Follow mode |
diff --git a/lisp/emacs-lisp/map.el b/lisp/emacs-lisp/map.el index 78cedd3ab10..d5051fcd98a 100644 --- a/lisp/emacs-lisp/map.el +++ b/lisp/emacs-lisp/map.el | |||
| @@ -95,12 +95,13 @@ Returns the result of evaluating the form associated with MAP-VAR's type." | |||
| 95 | (t (error "Unsupported map type `%S': %S" | 95 | (t (error "Unsupported map type `%S': %S" |
| 96 | (type-of ,map-var) ,map-var))))) | 96 | (type-of ,map-var) ,map-var))))) |
| 97 | 97 | ||
| 98 | (define-error 'map-not-inplace "Cannot modify map in-place: %S") | ||
| 99 | |||
| 98 | (cl-defgeneric map-elt (map key &optional default testfn) | 100 | (cl-defgeneric map-elt (map key &optional default testfn) |
| 99 | "Lookup KEY in MAP and return its associated value. | 101 | "Lookup KEY in MAP and return its associated value. |
| 100 | If KEY is not found, return DEFAULT which defaults to nil. | 102 | If KEY is not found, return DEFAULT which defaults to nil. |
| 101 | 103 | ||
| 102 | TESTFN is deprecated. Its default depends on the MAP argument. | 104 | TESTFN is deprecated. Its default depends on the MAP argument. |
| 103 | If MAP is a list, the default is `eql' to lookup KEY. | ||
| 104 | 105 | ||
| 105 | In the base definition, MAP can be an alist, hash-table, or array." | 106 | In the base definition, MAP can be an alist, hash-table, or array." |
| 106 | (declare | 107 | (declare |
| @@ -110,15 +111,16 @@ In the base definition, MAP can be an alist, hash-table, or array." | |||
| 110 | (macroexp-let2* nil | 111 | (macroexp-let2* nil |
| 111 | ;; Eval them once and for all in the right order. | 112 | ;; Eval them once and for all in the right order. |
| 112 | ((key key) (default default) (testfn testfn)) | 113 | ((key key) (default default) (testfn testfn)) |
| 113 | `(if (listp ,mgetter) | 114 | (funcall do `(map-elt ,mgetter ,key ,default) |
| 114 | ;; Special case the alist case, since it can't be handled by the | 115 | (lambda (v) |
| 115 | ;; map--put function. | 116 | `(condition-case nil |
| 116 | ,(gv-get `(alist-get ,key (gv-synthetic-place | 117 | ;; Silence warnings about the hidden 4th arg. |
| 117 | ,mgetter ,msetter) | 118 | (with-no-warnings (map-put! ,mgetter ,key ,v ,testfn)) |
| 118 | ,default nil ,testfn) | 119 | (map-not-inplace |
| 119 | do) | 120 | ,(funcall msetter |
| 120 | ,(funcall do `(map-elt ,mgetter ,key ,default) | 121 | `(map-insert ,mgetter ,key ,v)))))))))) |
| 121 | (lambda (v) `(map-put! ,mgetter ,key ,v))))))))) | 122 | ;; `testfn' is deprecated. |
| 123 | (advertised-calling-convention (map key &optional default) "27.1")) | ||
| 122 | (map--dispatch map | 124 | (map--dispatch map |
| 123 | :list (alist-get key map default nil testfn) | 125 | :list (alist-get key map default nil testfn) |
| 124 | :hash-table (gethash key map default) | 126 | :hash-table (gethash key map default) |
| @@ -336,17 +338,36 @@ MAP can be a list, hash-table or array." | |||
| 336 | ;; FIXME: I wish there was a way to avoid this η-redex! | 338 | ;; FIXME: I wish there was a way to avoid this η-redex! |
| 337 | (cl-defmethod map-into (map (_type (eql list))) (map-pairs map)) | 339 | (cl-defmethod map-into (map (_type (eql list))) (map-pairs map)) |
| 338 | 340 | ||
| 339 | (cl-defgeneric map-put! (map key value) | 341 | (cl-defgeneric map-put! (map key value &optional testfn) |
| 340 | "Associate KEY with VALUE in MAP and return VALUE. | 342 | "Associate KEY with VALUE in MAP and return VALUE. |
| 341 | If KEY is already present in MAP, replace the associated value | 343 | If KEY is already present in MAP, replace the associated value |
| 342 | with VALUE." | 344 | with VALUE. |
| 345 | This operates by modifying MAP in place. | ||
| 346 | If it cannot do that, it signals the `map-not-inplace' error. | ||
| 347 | If you want to insert an element without modifying MAP, use `map-insert'." | ||
| 348 | ;; `testfn' only exists for backward compatibility with `map-put'! | ||
| 349 | (declare (advertised-calling-convention (map key value) "27.1")) | ||
| 343 | (map--dispatch map | 350 | (map--dispatch map |
| 344 | :list (let ((p (assoc key map))) | 351 | :list (let ((oldmap map)) |
| 345 | (if p (setcdr p value) | 352 | (setf (alist-get key map key nil (or testfn #'equal)) value) |
| 346 | (error "No place to change the mapping for %S" key))) | 353 | (unless (eq oldmap map) |
| 354 | (signal 'map-not-inplace (list map)))) | ||
| 347 | :hash-table (puthash key value map) | 355 | :hash-table (puthash key value map) |
| 356 | ;; FIXME: If `key' is too large, should we signal `map-not-inplace' | ||
| 357 | ;; and let `map-insert' grow the array? | ||
| 348 | :array (aset map key value))) | 358 | :array (aset map key value))) |
| 349 | 359 | ||
| 360 | (define-error 'map-inplace "Can only modify map in place: %S") | ||
| 361 | |||
| 362 | (cl-defgeneric map-insert (map key value) | ||
| 363 | "Return a new map like MAP except that it associates KEY with VALUE. | ||
| 364 | This does not modify MAP. | ||
| 365 | If you want to insert an element in place, use `map-put!'." | ||
| 366 | (if (listp map) | ||
| 367 | (cons (cons key value) map) | ||
| 368 | ;; FIXME: Should we signal an error or use copy+put! ? | ||
| 369 | (signal 'map-inplace (list map)))) | ||
| 370 | |||
| 350 | ;; There shouldn't be old source code referring to `map--put', yet we do | 371 | ;; There shouldn't be old source code referring to `map--put', yet we do |
| 351 | ;; need to keep it for backward compatibility with .elc files where the | 372 | ;; need to keep it for backward compatibility with .elc files where the |
| 352 | ;; expansion of `setf' may call this function. | 373 | ;; expansion of `setf' may call this function. |
diff --git a/test/lisp/emacs-lisp/map-tests.el b/test/lisp/emacs-lisp/map-tests.el index 885b09be985..4dd67d48d40 100644 --- a/test/lisp/emacs-lisp/map-tests.el +++ b/test/lisp/emacs-lisp/map-tests.el | |||
| @@ -76,13 +76,25 @@ Evaluate BODY for each created map. | |||
| 76 | 'b | 76 | 'b |
| 77 | '2)))) | 77 | '2)))) |
| 78 | 78 | ||
| 79 | (ert-deftest test-map-put () | 79 | (ert-deftest test-map-put! () |
| 80 | (with-maps-do map | 80 | (with-maps-do map |
| 81 | (setf (map-elt map 2) 'hello) | 81 | (setf (map-elt map 2) 'hello) |
| 82 | (should (eq (map-elt map 2) 'hello))) | 82 | (should (eq (map-elt map 2) 'hello))) |
| 83 | (with-maps-do map | 83 | (with-maps-do map |
| 84 | (map-put map 2 'hello) | 84 | (map-put map 2 'hello) |
| 85 | (should (eq (map-elt map 2) 'hello))) | 85 | (should (eq (map-elt map 2) 'hello))) |
| 86 | (with-maps-do map | ||
| 87 | (map-put! map 2 'hello) | ||
| 88 | (should (eq (map-elt map 2) 'hello)) | ||
| 89 | (if (not (hash-table-p map)) | ||
| 90 | (should-error (map-put! map 5 'value) | ||
| 91 | ;; For vectors, it could arguably signal | ||
| 92 | ;; map-not-inplace as well, but it currently doesn't. | ||
| 93 | :type (if (listp map) | ||
| 94 | 'map-not-inplace | ||
| 95 | 'error)) | ||
| 96 | (map-put! map 5 'value) | ||
| 97 | (should (eq (map-elt map 5) 'value)))) | ||
| 86 | (let ((ht (make-hash-table))) | 98 | (let ((ht (make-hash-table))) |
| 87 | (setf (map-elt ht 2) 'a) | 99 | (setf (map-elt ht 2) 'a) |
| 88 | (should (eq (map-elt ht 2) | 100 | (should (eq (map-elt ht 2) |
| @@ -92,7 +104,7 @@ Evaluate BODY for each created map. | |||
| 92 | (should (eq (map-elt alist 2) | 104 | (should (eq (map-elt alist 2) |
| 93 | 'a))) | 105 | 'a))) |
| 94 | (let ((vec [3 4 5])) | 106 | (let ((vec [3 4 5])) |
| 95 | (should-error (setf (map-elt vec 3) 6)))) | 107 | (should-error (setf (map-elt vec 3) 6)))) |
| 96 | 108 | ||
| 97 | (ert-deftest test-map-put-alist-new-key () | 109 | (ert-deftest test-map-put-alist-new-key () |
| 98 | "Regression test for Bug#23105." | 110 | "Regression test for Bug#23105." |
| @@ -105,9 +117,9 @@ Evaluate BODY for each created map. | |||
| 105 | (let ((alist (list (cons "a" 1) (cons "b" 2))) | 117 | (let ((alist (list (cons "a" 1) (cons "b" 2))) |
| 106 | ;; Make sure to use a non-eq "a", even when compiled. | 118 | ;; Make sure to use a non-eq "a", even when compiled. |
| 107 | (noneq-key (string ?a))) | 119 | (noneq-key (string ?a))) |
| 108 | (map-put alist noneq-key 3 'equal) | 120 | (map-put alist noneq-key 3 #'equal) |
| 109 | (should-not (cddr alist)) | 121 | (should-not (cddr alist)) |
| 110 | (map-put alist noneq-key 9) | 122 | (map-put alist noneq-key 9 #'eql) |
| 111 | (should (cddr alist)))) | 123 | (should (cddr alist)))) |
| 112 | 124 | ||
| 113 | (ert-deftest test-map-put-return-value () | 125 | (ert-deftest test-map-put-return-value () |