Skip to content

Commit 534b058

Browse files
committed
Improve map and map-ref.
- Check as we go instead of using `seq-uniq` immediately, which tests have shown to always faster for the tested 10, 100, and 1000 entries. - Allow `unique` to be evaluated at run time. Optimize when we know when it is `nil` or `non-nil` at compile time. - Clarify Org docs.
1 parent a374774 commit 534b058

File tree

4 files changed

+109
-35
lines changed

4 files changed

+109
-35
lines changed

doc/loopy-doc.org

+6-5
Original file line numberDiff line numberDiff line change
@@ -1978,9 +1978,10 @@ source sequences.
19781978
order in which the key-value pairs are found. There is no guarantee that they
19791979
be in the same order each time.
19801980

1981-
These pairs are created before the loop begins. In other words, the map
1982-
=EXPR= is not processed progressively, but all at once. Therefore, this
1983-
command can have a noticeable start-up cost when working with very large maps.
1981+
These pairs are created before the loop begins via ~map-pairs~. In other
1982+
words, the map =EXPR= is not processed progressively, but all at once.
1983+
Therefore, this command can have a noticeable start-up cost when working with
1984+
very large maps.
19841985

19851986
#+begin_src emacs-lisp
19861987
;; => ((a . 1) (b . 2))
@@ -2426,8 +2427,8 @@ the accessed index during the loop.
24262427
of other commands. This is not the same as the =key= keyword parameter of the
24272428
accumulation commands.
24282429

2429-
Like in the command =map=, the keys of the map are generated before the
2430-
loop is run, which can be expensive for large maps.
2430+
Like in the command =map=, the keys of the map are generated via the function
2431+
~map-keys~ before the loop is run, which can be expensive for large maps.
24312432

24322433
Similar to =map=, any duplicate keys are ignored by default. This can be
24332434
disabled by setting the =unique= keyword argument to nil, though note that

doc/loopy.texi

+10-9
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ You should keep in mind that commands are evaluated in order. This means that
705705
attempting something like the below example might not do what you expect, as @samp{i}
706706
is assigned a value from the list after collecting @samp{i} into @samp{coll}.
707707

708-
@float Listing,org5119497
708+
@float Listing,orgfa9540f
709709
@lisp
710710
;; => (nil 1 2)
711711
(loopy (collect coll i)
@@ -887,7 +887,7 @@ the flag @samp{dash} provided by the package @samp{loopy-dash}.
887887

888888
Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
889889

890-
@float Listing,orgf3a9f7c
890+
@float Listing,orgb706e77
891891
@lisp
892892
;; => (1 2 3 4)
893893
(cl-loop for (i . j) in '((1 . 2) (3 . 4))
@@ -902,7 +902,7 @@ Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
902902
@caption{Destructuring values in a list.}
903903
@end float
904904

905-
@float Listing,org1bf5dc0
905+
@float Listing,orgcc60e47
906906
@lisp
907907
;; => (1 2 3 4)
908908
(cl-loop for elem in '((1 . 2) (3 . 4))
@@ -2150,9 +2150,10 @@ In general, as a map in not necessarily a sequence, you should not rely on the
21502150
order in which the key-value pairs are found. There is no guarantee that they
21512151
be in the same order each time.
21522152

2153-
These pairs are created before the loop begins. In other words, the map
2154-
@samp{EXPR} is not processed progressively, but all at once. Therefore, this
2155-
command can have a noticeable start-up cost when working with very large maps.
2153+
These pairs are created before the loop begins via @code{map-pairs}. In other
2154+
words, the map @samp{EXPR} is not processed progressively, but all at once.
2155+
Therefore, this command can have a noticeable start-up cost when working with
2156+
very large maps.
21562157

21572158
@lisp
21582159
;; => ((a . 1) (b . 2))
@@ -2629,8 +2630,8 @@ place referred to by @samp{VAR}. This is similar to the @samp{index} keyword pa
26292630
of other commands. This is not the same as the @samp{key} keyword parameter of the
26302631
accumulation commands.
26312632

2632-
Like in the command @samp{map}, the keys of the map are generated before the
2633-
loop is run, which can be expensive for large maps.
2633+
Like in the command @samp{map}, the keys of the map are generated via the function
2634+
@code{map-keys} before the loop is run, which can be expensive for large maps.
26342635

26352636
Similar to @samp{map}, any duplicate keys are ignored by default. This can be
26362637
disabled by setting the @samp{unique} keyword argument to nil, though note that
@@ -4653,7 +4654,7 @@ using the @code{let*} special form.
46534654
This method recognizes all commands and their aliases in the user option
46544655
@code{loopy-aliases}.
46554656

4656-
@float Listing,orgb546acb
4657+
@float Listing,orgab882ee
46574658
@lisp
46584659
;; => ((1 2 3) (-3 -2 -1) (0))
46594660
(loopy-iter (arg accum-opt positives negatives other)

loopy-commands.el

+67-21
Original file line numberDiff line numberDiff line change
@@ -897,50 +897,96 @@ BY is the function to use to move through the list (default `cdr')."
897897
(setq ,list-val (funcall ,list-func ,list-val)))))))
898898

899899
;;;;;; Map
900-
;; TODO: Instead of using `seq-uniq' at the start,
901-
;; check as we go.
902900
(cl-defun loopy--parse-map-command ((name var val &key (unique t)))
903-
"Parse the `map' loop command.
901+
"Parse the `map' loop command as `(map VAR EXPR &key (unique t))'.
904902
905903
Iterates through an alist of (key . value) dotted pairs,
906904
extracted from a hash-map, association list, property list, or
907905
vector using the library `map.el'."
908906
(when loopy--in-sub-level
909907
(loopy--signal-bad-iter name 'map))
910-
(let ((value-holder (gensym "map-")))
911-
`((loopy--iteration-vars
912-
(,value-holder ,(if unique
913-
`(seq-uniq (map-pairs ,val) #'loopy--car-equal-car)
914-
`(map-pairs ,val))))
915-
,@(loopy--destructure-for-iteration-command var `(car ,value-holder))
916-
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
908+
;; Not sure if this is needed, but want to be careful since we could have been
909+
;; incorrectly using pre-macro-expansion forms in the comparison.
910+
(setq unique (macroexpand-all unique macroexpand-all-environment))
911+
(loopy--instr-let-var* ((value-holder `(map-pairs ,val)))
912+
loopy--iteration-vars
913+
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
917914
;; at least for some commands.
918915
(loopy--pre-conditions (consp ,value-holder))
919-
(loopy--latter-body (setq ,value-holder (cdr ,value-holder))))))
916+
(loopy--latter-body (setq ,value-holder (cdr ,value-holder)))
917+
,@(cond
918+
((null unique)
919+
(loopy--destructure-for-iteration-command var `(car ,value-holder)))
920+
;; If UNIQUE is not evaluable code and is not `nil', then we know that
921+
;; we can use `member' directly.
922+
((and (macroexp-const-p unique)
923+
unique)
924+
(loopy--instr-let-var* ((key-list nil))
925+
loopy--iteration-vars
926+
(loopy--destructure-for-iteration-command
927+
var `(progn
928+
(while (member (caar ,value-holder) ,key-list)
929+
(setq ,value-holder (cdr ,value-holder)))
930+
(push (caar ,value-holder) ,key-list)
931+
(car ,value-holder)))))
932+
(t
933+
(loopy--instr-let-var* ((key-list nil)
934+
(test-fn `(if ,unique
935+
#'member
936+
#'ignore)))
937+
loopy--iteration-vars
938+
(loopy--destructure-for-iteration-command
939+
var `(progn
940+
(while (funcall ,test-fn (caar ,value-holder) ,key-list)
941+
(setq ,value-holder (cdr ,value-holder)))
942+
(push (caar ,value-holder) ,key-list)
943+
(car ,value-holder)))))))))
920944

921945
;;;;;; Map-Ref
922946
(cl-defun loopy--parse-map-ref-command ((name var val &key key (unique t)))
923-
"Parse the `map-ref' command as (map-ref VAR VAL).
947+
"Parse the `map-ref' command as (map-ref VAR VAL &key key (unique t)).
924948
925949
KEY is a variable name in which to store the current key.
926950
927951
Uses `map-elt' as a `setf'-able place, iterating through the
928952
map's keys. Duplicate keys are ignored."
929953
(when loopy--in-sub-level
930954
(loopy--signal-bad-iter name 'map-ref))
931-
(let ((key-list (gensym "map-ref-keys")))
932-
`((loopy--iteration-vars (,key-list ,(if unique
933-
`(seq-uniq (map-keys ,val))
934-
`(map-keys ,val))))
955+
;; Not sure if this is needed, but want to be careful since we could have been
956+
;; incorrectly using pre-macro-expansion forms in the comparison.
957+
(setq unique (macroexpand-all unique macroexpand-all-environment))
958+
(loopy--instr-let-var* ((key-list `(map-keys ,val)))
959+
loopy--iteration-vars
960+
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
961+
;; at least for some commands.
962+
(loopy--pre-conditions (consp ,key-list))
963+
(loopy--latter-body (setq ,key-list (cdr ,key-list)))
964+
,@(cond
965+
;; We don't need to do anything if we don't care about uniqueness.
966+
((null unique) nil)
967+
;; If UNIQUE is not evaluable code and is not `nil', then we know that
968+
;; we can use `member' directly.
969+
((and (macroexp-const-p unique)
970+
unique)
971+
(loopy--instr-let-var* ((found-keys nil))
972+
loopy--iteration-vars
973+
`((loopy--main-body (while (member (car ,key-list) ,found-keys)
974+
(setq ,key-list (cdr ,key-list))))
975+
(loopy--main-body (push (car ,key-list) ,found-keys)))))
976+
(t
977+
(loopy--instr-let-var* ((found-keys nil)
978+
(test-fn `(if ,unique
979+
#'member
980+
#'ignore)))
981+
loopy--iteration-vars
982+
`((loopy--main-body (while (funcall ,test-fn (car ,key-list) ,found-keys)
983+
(setq ,key-list (cdr ,key-list))))
984+
(loopy--main-body (push (car ,key-list) ,found-keys))))))
935985
,@(when key
936986
`((loopy--iteration-vars (,key nil))
937987
(loopy--main-body (setq ,key (car ,key-list)))))
938988
,@(loopy--destructure-for-generalized-command
939-
var `(map-elt ,val ,(or key `(car ,key-list))))
940-
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
941-
;; at least for some commands.
942-
(loopy--pre-conditions (consp ,key-list))
943-
(loopy--latter-body (setq ,key-list (cdr ,key-list))))))
989+
var `(map-elt ,val ,(or key `(car ,key-list)))))))
944990

945991
;;;;;; Numbers
946992

tests/tests.el

+26
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,18 @@ Using numbers directly will use less variables and more efficient code."
17431743
:iter-bare ((map-pairs . mapping-pairs)
17441744
(collect . collecting)))
17451745

1746+
(loopy-deftest map-:unique-var
1747+
:doc "`:unique' it `t' by default. Test when `nil'."
1748+
:result '((a . 1) (a . 27) (b . 2) (c . 3))
1749+
:body ((with (cat nil))
1750+
(map-pairs pair '((a . 1) (a . 27) (b . 2) (c . 3)) :unique cat)
1751+
(collect coll pair)
1752+
(finally-return coll))
1753+
:loopy t
1754+
:iter-keyword (map-pairs collect)
1755+
:iter-bare ((map-pairs . mapping-pairs)
1756+
(collect . collecting)))
1757+
17461758
(loopy-deftest map-destructuring
17471759
:doc "Check that `map' implements destructuring, not destructuring itself."
17481760
:result '((a b) (1 2))
@@ -1813,6 +1825,20 @@ Using numbers directly will use less variables and more efficient code."
18131825
(do . ignore)
18141826
(collect . collecting)))
18151827

1828+
(loopy-deftest map-ref-:unique-var
1829+
:doc "Fist `:a' becomes 15 because it gets found twice by `setf'."
1830+
:result '(:a 15 :a 2 :b 10)
1831+
:body ((with (map (list :a 1 :a 2 :b 3))
1832+
(cat nil))
1833+
(map-ref i map :unique cat)
1834+
(do (cl-incf i 7))
1835+
(finally-return map))
1836+
:loopy t
1837+
:iter-keyword (map-ref do collect)
1838+
:iter-bare ((map-ref . mapping-ref)
1839+
(do . ignore)
1840+
(collect . collecting)))
1841+
18161842
(loopy-deftest map-ref-destr
18171843
:doc "Check that `map-ref' implements destructuring, not the destructuring itself."
18181844
:result [[7 8] [7 8]]

0 commit comments

Comments
 (0)