picl

2024-10-12

Python Itertools in Common Lisp

Upstream URL

github.com/anlsh/picl

Author

Anish Moorthy <anlsh@protonmail.com>

License

MIT
README

picl Run-Tests Actions Status

Anish Moorthy anlsh@protonmail.com

Python Itertools in Common Lisp (v1.0.0). Pronounced like "pickle"

A (very nearly) complete port of Python's itertools package, complete with laziness where applicable.

This project currently lives on Github. Pull requests welcome!

Objectives and Rationale

PICL aims to provide a complete port of itertools, complete with laziness, without any reliance on cl-cont.

cl-itertools and snakes, provide similar functionality. Unfortunately both libraries rely on cl-cont, meaning they wont always play nice with the condition system, and cl-itertools remains very incomplete on top of that

Installation

PICL is in Quicklisp, and can be installed as follows

(ql:quickload :picl)

Do not :use this package: it might export new symbols in the future. You have been forewarned.

Documentation

Thanks to Staple you can read the documentation online or build it yourself like so

(staple:generate :picl :if-exists :supersede)

If you don't have PICL's dependencies loaded into your image yet, you'll get some harmless warnings about invalid definitions

Testing

A fairly comprehensive test suite written with FiveAM is provided. You can run it yourself either manually or through asdf

;; The easy way
(asdf:test-system :picl)
;; The slightly less easy way
(ql:quickload :picl/tests)
(fiveam:run! 'picl/tests:suite)

Concepts and How-To

An "iterator" in PICL is simply a thunk producing two values: the payload and the alive-indicator. The alive-indicator should be truthy until after the iterator is consumed.

By example

(let ((it (make-iterator '(1 2))))
  (next it)  ;; (values 1 t)
  (next it)  ;; (values 2 t)
  (next it)) ;; (values nil nil)

After returning nil, all further next calls should also produce nil as quickly as possible. Furthermore when the alive indicator is nil, the payload should be ignored.

To create iterators over your own objects, specialize the make-iterator generic function appropriately. For instance, the make-iterator definition for lists is

(defmethod make-iterator ((obj list))
  (lambda ()
    (if obj
        (values (prog1 (car obj) (setf obj (cdr obj))) t)
        (values nil nil))))

Specializations for lists and vectors are predefined. A universal in-it driver is also provided for Iterate through the picl/iterate system.

(ql:quickload '(:picl :picl/iterate))
;; The "iterate" package has been :use'd here
(iterate
    (for i in-it (picl:permutations '(1 2 3)))
    (collect i))
;; (#(1 2 3) #(1 3 2) #(2 1 3) #(2 3 1) #(3 1 2) #(3 2 1))

Note: All of the combinatoric iterators produce vectors, which can be annoying because those are second-class citizens in CL (you can't destructure them, for instance). To get around this, you can wrap the iterator in (picl:map #'iter-to-list <>)

(picl:iter-to-list (picl:map #'picl:iter-to-list (picl:permutations '(1 2 3))))
;; ((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1))

It's a bit clunky for sure, so in the future I might extend the in-it clause to perform conversions like this when specified

Future Work

Functions still missing from Python's itertools (due to laziness: if you need these drop an issue/PR and I'll get around to implementing them)

Extensions to library

  • Port the more-itertools recipes found at bottom of the Python itertools

package

(seems like a big job)

  • Some sort of integration with fset's

sequence type?

License

This project is provided under the MIT License (see LICENSE.md)

Dependencies (5)

  • alexandria
  • defclass-std
  • fiveam
  • generic-cl
  • iterate

Dependents (0)

    • GitHub
    • Quicklisp