Bump version to 0.0.1
Use better default parser for input-stream-iterator
Update readme
Ergonomic: It is a joy to use and has a consistent interface unlike cl:loop
, iterate
, series
, and ANSI list operations (cl:mapcar
, cl:remove-if-not
, cl:reduce
, cl:dolist
). 2 interfaces are provided that are compatible with each other; use whichever one pleases you most.
Extensible: You can define your own iterators and aggregators that integrate tightly into other operations.
Efficient: All operations are non-consing when possible. It seems to be slightly slower than existing looping patterns, but the performance is comparable. Please contribute if you know how to make it faster.
Loop uses some symbols in the common-lisp
package, so you can use package local nicknames or a :shadowing-import
. These examples are in the charje.loop.user
package.
Example of regular loop.
(loop (n (step :to 15))
(when (oddp n)
(averaging n)))
;; 8
The same example without any local variables making use of binding-arrows.
(-> (step :to 15)
(filter #'oddp)
average)
;; 8
Looping over a list and a vector at the same time.
(loop ((list-number (list 1 2 3))
(vector-number (vector 4 5 6)))
(print (expt list-number vector-number)))
The same example with the iteration functions. Notice that the operations work with multiple values. Notice also that loop*
is just like loop
but for use in binding-arrows:->
.
(-> (list 1 2 3) iterator
(zip (iterator (vector 4 5 6)))
(map #'expt)
(loop* (n)
(print n)))
see cookbook.md for more examples with comparisons to ANSI constructs.
An iterator is the input to loop
and loop*
. For loop the results of each iterator get bound to a loop variable. You can leave off the extra set of parenthesis if there is only 1 iterator. loop*
takes an iterator as its first argument; this is why it works with binding-arrows:->
. If an iterator returns multiple values, then just put more than one binding.
(loop ((a <iterator>)
(b c <iterator>))
...)
(loop (a <iterator>)
...)
(loop* <iterator> (a b)
...)
An iterator is a function that takes zero arguments and returns the next value (or values) of iteration. When (if) the iterator runs out of values it should (throw 'end-of-iteration nil)
; this is done most easily with the end-iteration
function.
An iterator constructor is some means of creating a iterator. It may take anything as arguments including another iterator. By these definitions, there is an iterator built into ANSI CL: constantly
which takes a value as input and returns an iterator that returns that value forever.
Iterators should be non-consing. Iterators should be inlined. Iterators that deal with other iterators should account for them returning multiple values. Creating an iteration should not really do anything on its own. Real action should happen when loop
starts calling next
(funcall
) on the iterators. Documentation for iterators should talk about them as if they are not lazy.
Loop uses the iterator
generic-function to convert data to an iterator. Feel free to define iterator
methods for your custom data structures.
See iterators.lisp for examples.
Aggregators while not necessary are a convenient way to aggregate some data. Aggregators can be defined most conveniently using define-aggregation-macro
(see the docstring for more details). A function version of the aggregator for use in binding-arrows:->
can then be made trivially using loop
.
Aggregators can tell loop
to create local variables and symbol-macrolet
s.
see aggregators.lisp for examples.
(progn
(put 'loop 'sly-common-lisp-indent-function 1)
(put 'loop* 'sly-common-lisp-indent-function 1))
Ideally we can get as good as or close to cl:loop
(fast, non-consing). I at least want to be able to say "O(1) consing".
(defconstant eight-nines 99999999)
(defconstant nine-nines 999999999)
(defconstant ten-nines 9999999999)
built in functional list API crashes
(binding-arrows:->>
(cl:loop for n from 0 to eight-nines collect n)
(remove-if-not #'evenp)
(mapcar #'sqrt)
(cl:reduce #'+))
iterator API 3.476 seconds of real time 14,600,014,046 processor cycles 1,600,021,360 bytes consed
after inlining all functions 1.136 seconds of real time 4,774,886,650 processor cycles 1,599,993,760 bytes consed
after declare values (in filter) as dynamic-extent 1.132 seconds of real time 4,745,768,101 processor cycles 0 bytes consed
after optimizing step 0.992 seconds of real time 4,166,720,852 processor cycles 0 bytes consed
after making a loop that doesn't use generic functions 0.864 seconds of real time 3,630,327,955 processor cycles 0 bytes consed
(binding-arrows:->
(step :to eight-nines)
(filter #'evenp)
(map #'sqrt)
(reduce #'+ 0))
built in loop 0.316 seconds of real time 1,320,238,807 processor cycles 0 bytes consed
(cl:loop for n from 0 to eight-nines
when (evenp n)
summing (sqrt n))
iterating loop 2.024 seconds of real time 8,503,209,645 processor cycles 32,752 bytes consed
after inlining step 1.008 seconds of real time 4,225,760,855 processor cycles 0 bytes consed
after optimizing step 0.776 seconds of real time 3,260,816,875 processor cycles 0 bytes consed
(loop (n (step :to eight-nines))
(when (evenp n)
(summing (sqrt n))))
declare fixnum (cl:loop
did not improve significantly from declaring fixnum)
0.804 seconds of real time
3,377,967,840 processor cycles
0 bytes consed
after fixing declarations 0.544 seconds of real time 2,274,290,234 processor cycles 0 bytes consed
(loop (n (step :to eight-nines))
(declare (fixnum n))
(when (evenp n)
(summing (sqrt n))))