branch: externals/dash commit 5281702e36e87164d2489abc95441b88ca1d4b10 Author: Basil L. Contovounesios <conto...@tcd.ie> Commit: Basil L. Contovounesios <conto...@tcd.ie>
Close low-flying zips * NEWS.md (2.20.0): Announce deprecation of dyadic -zip calling convention. * readme-template.md (Change log): Retract promise to break -zip (issue #400). * dash.el (--reject-first): Pacify Emacs 24 macro #'-quoting warning. (-remove-at, -zip-fill): Optimize. Extend docstring. (-remove-at-indices): Handle empty list input. Optimize. Clarify docstring. (--zip-with, -zip-lists): Simplify. Extend docstring. (-zip-with): Fix docstring. (-zip-lists-fill, -unzip-lists, dash--length=) (dash--zip-lists-or-pair): New functions. (-zip): Replace anonymous compiler-macro with dash--zip-lists-or-pair for the benefit of Emacs 24.3. Use dash--length= in place of length. Clarify docstring. (-zip-pair): Make variadic again for backward compatibility with when it used to be an alias of -zip, and declare a dyadic advertised-calling-convention instead. Delegate to -zip-lists as needed. Clarify docstring. (-unzip): Clarify docstring (issue #400). (dash--match-kv-normalize-match-form, -let): Simplify. (-prodfn): Optimize. Fixes #400. --- NEWS.md | 11 +- dash.el | 288 +++++++++++++++++++++++++++++++++++------------------ readme-template.md | 13 --- 3 files changed, 200 insertions(+), 112 deletions(-) diff --git a/NEWS.md b/NEWS.md index ab08c1ee76..5ac57ea155 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,13 @@ See the end of the file for license conditions. ### From 2.19.1 to 2.20.0 +#### Deprecations + +- Calling `-zip` with two arguments now emits a warning. This + long-discouraged calling convention remains supported, but the + caller is now referred to the equivalent `-zip-pair` instead (Stefan + Monnier, #400). + #### Fixes - Fixed a regression from `2.18` in `-take` that caused it to @@ -132,8 +139,8 @@ https://github.com/magnars/dash.el/wiki/Obsoletion-of-dash-functional.el - Sped up `-uniq` by using hash-tables when possible (@cireu, #305). - Fixed `-inits` to be non-destructive (@SwiftLawnGnome, #313). - Fixed indent rules for `-some->` and family (@wbolster, #321). -- Added `-zip-lists` which always returns a list of proper lists, even for two - input lists (see issue #135). +- Added `-zip-lists` which always returns a list of proper lists, even + for two input lists, in contrast to `-zip` (see issue #135). ### From 2.15 to 2.16 diff --git a/dash.el b/dash.el index 375b4cdc93..ef4f9d3263 100644 --- a/dash.el +++ b/dash.el @@ -536,8 +536,9 @@ This function's anaphoric counterpart is `--remove-first'. See also `-map-first', `-remove-item', and `-remove-last'." (--remove-first (funcall pred it) list)) +;; TODO: #'-quoting the macro upsets Emacs 24. (defalias '-reject-first #'-remove-first) -(defalias '--reject-first #'--remove-first) +(defalias '--reject-first '--remove-first) (defmacro --remove-last (form list) "Remove the last item from LIST for which FORM evals to non-nil. @@ -1298,28 +1299,41 @@ See also: `-map-when'" `(-update-at ,n (lambda (it) (ignore it) ,form) ,list)) (defun -remove-at (n list) - "Return a list with element at Nth position in LIST removed. + "Return LIST with its element at index N removed. +That is, remove any element selected as (nth N LIST) from LIST +and return the result. -See also: `-remove-at-indices', `-remove'" +This is a non-destructive operation: parts of LIST (but not +necessarily all of it) are copied as needed to avoid +destructively modifying it. + +See also: `-remove-at-indices', `-remove'." (declare (pure t) (side-effect-free t)) - (-remove-at-indices (list n) list)) + (if (zerop n) + (cdr list) + (--remove-first (= it-index n) list))) (defun -remove-at-indices (indices list) - "Return a list whose elements are elements from LIST without -elements selected as `(nth i list)` for all i -from INDICES. + "Return LIST with its elements at INDICES removed. +That is, for each index I in INDICES, remove any element selected +as (nth I LIST) from LIST. -See also: `-remove-at', `-remove'" +This is a non-destructive operation: parts of LIST (but not +necessarily all of it) are copied as needed to avoid +destructively modifying it. + +See also: `-remove-at', `-remove'." (declare (pure t) (side-effect-free t)) - (let* ((indices (-sort '< indices)) - (diffs (cons (car indices) (-map '1- (-zip-with '- (cdr indices) indices)))) - r) - (--each diffs - (let ((split (-split-at it list))) - (!cons (car split) r) - (setq list (cdr (cadr split))))) - (!cons list r) - (apply #'-concat (nreverse r)))) + (setq indices (--drop-while (< it 0) (-sort #'< indices))) + (let ((i (pop indices)) res) + (--each-while list i + (pop list) + (if (/= it-index i) + (push it res) + (while (and indices (= (car indices) i)) + (pop indices)) + (setq i (pop indices)))) + (nconc (nreverse res) list))) (defmacro --split-with (pred list) "Anaphoric form of `-split-with'." @@ -1603,115 +1617,192 @@ elements of LIST. Keys are compared by `equal'." (nreverse result)))) (defmacro --zip-with (form list1 list2) - "Anaphoric form of `-zip-with'. + "Zip LIST1 and LIST2 into a new list according to FORM. +That is, evaluate FORM for each item pair from the two lists, and +return the list of results. The result is as long as the shorter +list. + +Each element of LIST1 and each element of LIST2 in turn are bound +pairwise to `it' and `other', respectively, and their index +within the list to `it-index', before evaluating FORM. -Each element in turn of LIST1 is bound to `it', and of LIST2 to -`other', before evaluating FORM." +This is the anaphoric counterpart to `-zip-with'." (declare (debug (form form form))) (let ((r (make-symbol "result")) - (l1 (make-symbol "list1")) (l2 (make-symbol "list2"))) - `(let ((,r nil) - (,l1 ,list1) - (,l2 ,list2)) - (while (and ,l1 ,l2) - (let ((it (car ,l1)) - (other (car ,l2))) - (!cons ,form ,r) - (!cdr ,l1) - (!cdr ,l2))) + `(let ((,l2 ,list2) ,r) + (--each-while ,list1 ,l2 + (let ((other (pop ,l2))) + (ignore other) + (push ,form ,r))) (nreverse ,r)))) (defun -zip-with (fn list1 list2) - "Zip the two lists LIST1 and LIST2 using a function FN. This -function is applied pairwise taking as first argument element of -LIST1 and as second argument element of LIST2 at corresponding -position. + "Zip LIST1 and LIST2 into a new list using the function FN. +That is, apply FN pairwise taking as first argument the next +element of LIST1 and as second argument the next element of LIST2 +at the corresponding position. The result is as long as the +shorter list. -The anaphoric form `--zip-with' binds the elements from LIST1 as symbol `it', -and the elements from LIST2 as symbol `other'." +This function's anaphoric counterpart is `--zip-with'. + +For other zips, see also `-zip-lists' and `-zip-fill'." (--zip-with (funcall fn it other) list1 list2)) (defun -zip-lists (&rest lists) - "Zip LISTS together. Group the head of each list, followed by the -second elements of each list, and so on. The lengths of the returned -groupings are equal to the length of the shortest input list. + "Zip LISTS together. + +Group the head of each list, followed by the second element of +each list, and so on. The number of returned groupings is equal +to the length of the shortest input list, and the length of each +grouping is equal to the number of input LISTS. -The return value is always list of lists, which is a difference -from `-zip-pair' which returns a cons-cell in case two input -lists are provided. +The return value is always a list of proper lists, in contrast to +`-zip' which returns a list of dotted pairs when only two input +LISTS are provided. -See also: `-zip'" +See also: `-zip-pair'." (declare (pure t) (side-effect-free t)) (when lists (let (results) - (while (-none? 'null lists) - (setq results (cons (mapcar 'car lists) results)) - (setq lists (mapcar 'cdr lists))) + (while (--every it lists) + (push (mapcar #'car lists) results) + (setq lists (mapcar #'cdr lists))) (nreverse results)))) +(defun -zip-lists-fill (fill-value &rest lists) + "Zip LISTS together, padding shorter lists with FILL-VALUE. +This is like `-zip-lists' (which see), except it retains all +elements at positions beyond the end of the shortest list. The +number of returned groupings is equal to the length of the +longest input list, and the length of each grouping is equal to +the number of input LISTS." + (declare (pure t) (side-effect-free t)) + (when lists + (let (results) + (while (--some it lists) + (push (--map (if it (car it) fill-value) lists) results) + (setq lists (mapcar #'cdr lists))) + (nreverse results)))) + +(defun -unzip-lists (lists) + "Unzip LISTS. + +This works just like `-zip-lists' (which see), but takes a list +of lists instead of a variable number of arguments, such that + + (-unzip-lists (-zip-lists ARGS...)) + +is identity (given that the lists comprising ARGS are of the same +length)." + (declare (pure t) (side-effect-free t)) + (apply #'-zip-lists lists)) + +(defalias 'dash--length= + (if (fboundp 'length=) + #'length= + (lambda (list length) + (cond ((< length 0) nil) + ((zerop length) (null list)) + ((let ((last (nthcdr (1- length) list))) + (and last (null (cdr last)))))))) + "Return non-nil if LIST is of LENGTH. +This is a compatibility shim for `length=' in Emacs 28. +\n(fn LIST LENGTH)") + +(defun dash--zip-lists-or-pair (_form &rest lists) + "Return a form equivalent to applying `-zip' to LISTS. +This `compiler-macro' warns about discouraged `-zip' usage and +delegates to `-zip-lists' or `-zip-pair' depending on the number +of LISTS." + (if (not (dash--length= lists 2)) + (cons #'-zip-lists lists) + (let ((pair (cons #'-zip-pair lists)) + (msg "Use -zip-pair instead of -zip to get a list of pairs")) + (if (fboundp 'macroexp-warn-and-return) + (macroexp-warn-and-return msg pair) + (message msg) + pair)))) + (defun -zip (&rest lists) - "Zip LISTS together. Group the head of each list, followed by the -second elements of each list, and so on. The lengths of the returned -groupings are equal to the length of the shortest input list. - -If two lists are provided as arguments, return the groupings as a list -of cons cells. Otherwise, return the groupings as a list of lists. - -Use `-zip-lists' if you need the return value to always be a list -of lists. - -Alias: `-zip-pair' - -See also: `-zip-lists'" - (declare (pure t) (side-effect-free t) - (compiler-macro - (lambda (form) - (if (not (= 2 (length lists))) - (cons #'-zip-lists lists) - (let ((msg "Use -zip-pair to get a list of pairs")) - (if (fboundp 'macroexp-warn-and-return) - (macroexp-warn-and-return - msg (cons #'-zip-pair lists)) - (message msg) - form)))))) - ;; To support backward compatibility, return - ;; cons cells if two lists were provided. - (apply (if (= (length lists) 2) #'-zip-pair #'-zip-lists) lists)) - -(defun -zip-pair (list1 list2) + "Zip LISTS together. + +Group the head of each list, followed by the second element of +each list, and so on. The number of returned groupings is equal +to the length of the shortest input list, and the number of items +in each grouping is equal to the number of input LISTS. + +If only two LISTS are provided as arguments, return the groupings +as a list of dotted pairs. Otherwise, return the groupings as a +list of proper lists. + +Since the return value changes form depending on the number of +arguments, it is generally recommended to use `-zip-lists' +instead, or `-zip-pair' if a list of dotted pairs is desired. + +See also: `-unzip'." + (declare (compiler-macro dash--zip-lists-or-pair) + (pure t) (side-effect-free t)) + ;; For backward compatibility, return a list of dotted pairs if two + ;; arguments were provided. + (apply (if (dash--length= lists 2) #'-zip-pair #'-zip-lists) lists)) + +(defun -zip-pair (&rest lists) "Zip LIST1 and LIST2 together. -Make a pair of the head of each list, followed by the -second elements of each list, and so on. The returned -groupings are equal to the length of the shortest input list. -See also: `-zip-lists'" - (--map (cons (car it) (cadr it)) (-zip-lists list1 list2))) +Make a pair with the head of each list, followed by a pair with +the second element of each list, and so on. The number of pairs +returned is equal to the length of the shorter input list. + +See also: `-zip-lists'." + (declare (advertised-calling-convention (list1 list2) "2.20.0") + (pure t) (side-effect-free t)) + (if (dash--length= lists 2) + (--zip-with (cons it other) (car lists) (cadr lists)) + (apply #'-zip-lists lists))) (defun -zip-fill (fill-value &rest lists) - "Zip LISTS, with FILL-VALUE padded onto the shorter lists. The -lengths of the returned groupings are equal to the length of the -longest input list." + "Zip LISTS together, padding shorter lists with FILL-VALUE. +This is like `-zip' (which see), except it retains all elements +at positions beyond the end of the shortest list. The number of +returned groupings is equal to the length of the longest input +list, and the length of each grouping is equal to the number of +input LISTS. + +Since the return value changes form depending on the number of +arguments, it is generally recommended to use `-zip-lists-fill' +instead, unless a list of dotted pairs is explicitly desired." (declare (pure t) (side-effect-free t)) - (apply #'-zip (apply #'-pad (cons fill-value lists)))) + (cond ((null lists) ()) + ((dash--length= lists 2) + (let ((list1 (car lists)) + (list2 (cadr lists)) + results) + (while (or list1 list2) + (push (cons (if list1 (pop list1) fill-value) + (if list2 (pop list2) fill-value)) + results)) + (nreverse results))) + ((apply #'-zip-lists-fill fill-value lists)))) (defun -unzip (lists) "Unzip LISTS. -This works just like `-zip' but takes a list of lists instead of -a variable number of arguments, such that +This works just like `-zip' (which see), but takes a list of +lists instead of a variable number of arguments, such that (-unzip (-zip L1 L2 L3 ...)) -is identity (given that the lists are the same length). +is identity (given that the lists are of the same length, and +that `-zip' is not called with two arguments, because of the +caveat described in its docstring). -Note in particular that calling this on a list of two lists will -return a list of cons-cells such that the above identity works. +Note in particular that calling `-unzip' on a list of two lists +will return a list of dotted pairs. -See also: `-zip'" - ;; FIXME: Huh? AFAIK `-zip' is not its own inverse. - ;; (-unzip (-zip '(1 2 3) '(a b c))) - ;; gives me "Wrong type argument: listp, a" +Since the return value changes form depending on the number of +LISTS, it is generally recommended to use `-unzip-lists' instead." + (declare (pure t) (side-effect-free t)) (apply #'-zip lists)) (defun -cycle (list) @@ -2228,7 +2319,7 @@ This method normalizes PATTERN to the format expected by (let ((normalized (list (car pattern))) (skip nil) (fill-placeholder (make-symbol "--dash-fill-placeholder--"))) - (-each (apply #'-zip (-pad fill-placeholder (cdr pattern) (cddr pattern))) + (-each (-zip-fill fill-placeholder (cdr pattern) (cddr pattern)) (lambda (pair) (let ((current (car pair)) (next (cdr pair))) @@ -2567,7 +2658,8 @@ because we need to support improper list binding." ,@body) (let* ((varlist (dash--normalize-let-varlist varlist)) (inputs (--map-indexed (list (make-symbol (format "input%d" it-index)) (cadr it)) varlist)) - (new-varlist (--map (list (caar it) (cadr it)) (-zip-pair varlist inputs)))) + (new-varlist (--zip-with (list (car it) (car other)) + varlist inputs))) `(let ,inputs (-let* ,new-varlist ,@body))))) @@ -3742,7 +3834,8 @@ This function satisfies the following laws: (-compose (-partial #\\='nth n) (-prod f1 f2 ...)) = (-compose fn (-partial #\\='nth n))" - (lambda (x) (-zip-with 'funcall fns x))) + (declare (pure t) (side-effect-free t)) + (lambda (x) (--zip-with (funcall it other) fns x))) ;;; Font lock @@ -3868,7 +3961,8 @@ which do not dynamically detect macros, Dash-Fontify mode additionally fontifies Dash macro calls. See also `dash-fontify-mode-lighter' and -`global-dash-fontify-mode'." :lighter dash-fontify-mode-lighter +`global-dash-fontify-mode'." + :lighter dash-fontify-mode-lighter (if dash-fontify-mode (font-lock-add-keywords nil dash--keywords t) (font-lock-remove-keywords nil dash--keywords)) diff --git a/readme-template.md b/readme-template.md index 3065d2c80a..ef33241438 100644 --- a/readme-template.md +++ b/readme-template.md @@ -14,7 +14,6 @@ See the end of the file for license conditions. ## Contents * [Change log](#change-log) - * [Upcoming breaking change!](#upcoming-breaking-change) * [Installation](#installation) * [Functions](#functions) * [Contribute](#contribute) @@ -25,18 +24,6 @@ See the end of the file for license conditions. See the [`NEWS.md`](NEWS.md) file. -### Upcoming breaking change! - -- For backward compatibility reasons, `-zip` when called with two - lists returns a list of cons cells, rather than a list of proper - lists. This is a clunky API, and may be changed in a future release - to always return a list of proper lists, as `-zip-lists` currently - does. - - **N.B.:** Do not rely on the current behavior of `-zip` for two - lists. Instead, use `-zip-pair` for a list of cons cells, and - `-zip-lists` for a list of proper lists. - ## Installation Dash is available on [GNU ELPA](https://elpa.gnu.org/), [GNU-devel