branch: elpa/treepy
commit 237f97160a685f5c7c80908bf1f28be08f13e2cb
Author: Daniel Barreto <[email protected]>
Commit: Daniel Barreto <[email protected]>
Rework README.md
---
README.md | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
treepy.el | 44 +++++-----
2 files changed, 316 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 6eac262477..0c67776312 100644
--- a/README.md
+++ b/README.md
@@ -3,5 +3,297 @@
A set of generic functions for traversing tree-like data structures recursively
and/or iteratively, ported
from [`clojure.walk`](https://clojure.github.io/clojure/clojure.walk-api.html)
-and [`clojure.zipper`](http://clojure.github.io/clojure/clojure.zip-api.html)
+and [`clojure.zip`](http://clojure.github.io/clojure/clojure.zip-api.html)
respectively.
+
+## Main differences with clojure libraries
+
+Even though one of *treepy's* goals is to provide an API that's as close as
+possible
+to [`clojure.walk`](https://clojure.github.io/clojure/clojure.walk-api.html)
+and [`clojure.zip`](http://clojure.github.io/clojure/clojure.zip-api.html),
+there are some subtle (and not so subtle) differences derived from
elisp/clojure
+distinct data structures, levels of abstraction, and code conventions.
+
+The most notorious difference is the name of the functions. For every function
+in Clojure world, there's a *treepy* counterpart that's prefixed with
`treepy-`.
+So:
+
+- `clojure.walk/walk` -> `treepy-walk`
+- `clojure.zip/zipper` -> `treepy-zipper`
+
+... and so on.
+
+### clojure.walk
+
+- `treepy-walk` (and all its derivatives) works on lists, vectors, alists and
+ hash-tables only.
+
+- Instead of printing to stdout, `treepy-prewalk-demo` and
+ `treepy-postwalk-demo` return a list of the sub forms as they get walked.
+
+- *treepy* doesn't provide implementations for `keywordize-keys`,
+ `stringify-keys` and `macroexpand-all`. There's already
+ a
+ [`macroexpand-all` implementation built
in](https://www.gnu.org/software/emacs/manual/html_node/elisp/Expansion.html).
+
+- `treepy-prewalk-replace` and `treepy-postwalk-replace` are based on the
(Emacs
+ 25) built
+ in
+
[`map-contains-key`](https://github.com/emacs-mirror/emacs/blob/master/lisp/emacs-lisp/map.el#L262)
function.
+ Both functions take an optional a *testfn* third parameter to be used by
+ `map-contains-key`. Defaults to `equal`.
+
+### clojure.zip
+
+- In order to follow elisp conventions, *treepy* has a couple of other small
+ renamings:
+
+ * `clojure.zip/branch?` -> `treepy-branch-p`
+ * `clojure.zip/end?` -> `treepy-end-p`
+
+- There's is no exact equivalent to `clojure.zip/seq-zip` in *treepy*, a
+ `treepy-list-zip` is provided instead.
+
+- *treepy* provides a way to traverse a tree in postorder while using the
+ enumeration functions `treepy-next` and `treepy-prev`. This is done by having
+ an extra optional parameter *order* that can be passed to both functions:
+
+```
+(treepy-next loc) ;; => next node in preorder, as in clojure.zip/next.
+(treepy-next loc :preorder) ;; => also next node in preorder.
+(treepy-next loc :postorder) ;; => next node in postorder.
+```
+
+- There's a new function in *treepy* to get the leftmost descendant of a
+ node/loc. Unsuprisingly, it's called `treepy-leftmost-descendant`. This
function
+ is particularly useful when trying to traverse a tree in post order, since
+ unlike preorder trasversal, the root is NOT the first element you want
+ walk/visit. You might want to call `(treepy-leftmost-descendant root)` before
+ starting to walk the nodes with `treepy-next` in postorder.
+
+The following are some *treepy*'s implementation differences that you might not
+need to bother with if you just wanna use the library, so you can skip directly
+to the [available functions](#available-functions).
+
+- **treepy's *loc* data structure:** When you create a Clojure "zipper" with
+ `clojure.zip/zipper`, you have to provide three helper functions (`branch?`,
+ `children`, and `make-node`) and a `root` form. `clojure.zip/zipper` will
then
+ return a tuple vector that represents a "*loc*action". The three helper
+ functions are stored as
+ clojure's [metadata](https://clojure.org/reference/metadata) into the
returned
+ *loc*. Since there's no equivalent to metadata in Elisp, *treepy* directly
+ associates the three helper functions into an alist that's returned with the
+ rest of the *loc* information. The resulting structure of a *treepy* `loc` is
+ the following:
+
+```
+((<current node> . <path alist>) . ((:branch-p . #'provided-branch-fn)
+ (:children . #'provided-children-fn)
+ (:make-node . #'provided-make-node-fn)))
+```
+
+- `<path alist>` is an alist containing the same keys and values as
+ `clojure.zip`'s path map. Only difference is that *treepy* uses lists instead
+ of vectors to handle the `left` sibilings and `pnodes` parent nodes.
+
+## Available functions
+
+### Walker
+
+- `treepy-walk` inner outer form
+
+ Traverses FORM, an arbitrary data structure. INNER and OUTER are functions.
+ Applies INNER to each element of FORM, building up a data structure of the
+ same type, then applies OUTER to the result. Recognizes cons, lists, alists,
+ vectors and hash tables.
+
+- `treepy-postwalk` f form
+
+ Performs a depth-first, post-order traversal of F applied to FORM. Calls F on
+ each sub-form, uses F's return value in place of the original. Recognizes
+ cons, lists, alists, vectors and hash tables.
+
+- `treepy-prewalk` f form
+
+ Like `treepy-postwalk`, Performs function F on FORM but does pre-order
+ traversal.
+
+- `treepy-prewalk-demo` form
+
+ Demonstrates the behavior of `treepy-prewalk` for FORM. Returns a list of
+ each form as it is walked.
+
+- `treepy-postwalk-demo` form
+
+ Demonstrates the behavior of `treepy-postwalk` for FORM. Returns a list of
+ each form as it is walked.
+
+- `treepy-postwalk-replace` smap form &optional testfn
+
+ Recursively transforms FORM by replacing keys in SMAP with their values. Does
+ replacement at the root of the tree first. The optional TESTFN is passed to
+ `map-contains-key` as the testing equality function.
+
+- `treepy-postwalk-replace` smap form &optional testfn
+
+ Recursively transforms FORM by replacing keys in SMAP with their values. Does
+ replacement at the leaves of the tree first. The optional TESTFN is passed to
+ `map-contains-key` as the testing equality function.
+
+### Zipper
+
+#### Construction
+
+- `treepy-zipper` branchp children make-node root
+
+ Creates a new zipper structure.
+
+ BRANCHP is a function that, given a node, returns t if it can have children,
+ even if it currently doesn't.
+
+ CHILDREN is a function that, given a branch node, returns a seq of its
+ children.
+
+ MAKE-NODE is a function that, given an existing node and a seq of children,
+ returns a new branch node with the supplied children.
+
+ ROOT is the root node.
+
+- `treepy-list-zip` root
+
+ Returns a zipper for nested lists, given a ROOT list.
+
+- `treepy-vector-zip` root
+
+ Returns a zipper for nested vectors, given a ROOT vector.
+
+#### Context / Path
+
+- `treepy-node` loc
+
+ Returns the node at LOC.
+
+- `treepy-branch-p` loc
+
+ Returns `t` if the node at LOC is a branch. `nil` otherwise.
+
+- `treepy-children` loc
+
+ Returns a children list of the node at LOC, which must be a branch.
+
+- `treepy-make-node` loc node children
+
+ Returns a new branch node, given an existing LOC, NODE and new CHILDREN. The
+ LOC is only used to supply the constructor.
+
+- `treepy-path` loc
+
+ Returns a list of nodes leading to the given LOC.
+
+- `treepy-lefts` loc
+
+ Returns a list of the left sibilings of this LOC.
+
+- `treepy-rights` loc
+
+ Returns a list of the right sibilings of this LOC.
+
+#### Navigation
+
+- `treepy-down` loc
+
+ Returns the loc of the leftmost child of the node at this LOC, or `nil` if no
+ children.
+
+- `treepy-up` loc
+
+ Returns the loc of the parent of the node at this LOC, or `nil` if at the
top.
+
+- `treepy-root` loc
+
+ Zips from LOC all the way up and return the root node, reflecting any
changes.
+
+- `treepy-right` loc
+
+ Returns the loc of the right sibling of the node at this LOC, or `nil`.
+
+- `treepy-rightmost` loc
+
+ Returns the loc of the rightmost sibling of the node at this LOC, or self.
+
+- `treepy-left` loc
+
+ Returns the loc of the left sibling of the node at this LOC, or `nil`.
+
+- `treepy-leftmost` loc
+
+ Returns the loc of the leftmost sibling of the node at this LOC, or self.
+
+- `treepy-leftmost-descendant` loc
+
+ Returns the leftmost descendant of the given LOC. (ie, down repeatedly).
+
+#### Modification
+
+- `treepy-insert-left` loc item
+
+ Inserts ITEM as the left sibiling of this LOC'S node, without moving.
+
+- `treepy-insert-right` loc item
+
+ Inserts ITEM as the right sibling of this LOC's node, without moving.
+
+- `treepy-replace` loc node
+
+ Replaces the node in this LOC with the given NODE, without moving.
+
+- `treepy-edit` loc f &rest args
+
+ Replaces the node at this LOC with the value of `(F node ARGS)`.
+
+- `treepy-insert-child` loc item
+
+ Inserts ITEM as the leftmost child of this LOC's node, without moving.
+
+- `treepy-append-child` loc item
+
+ Inserts ITEM as the rightmost child of this LOC'S node, without moving.
+
+- `treepy-remove` loc
+
+ Removes the node at LOC. Returns the loc that would have preceded it in a
+ depth-first walk.
+
+#### Enumeration
+
+- `treepy-next` loc &optional order
+
+ Moves to the next LOC in the hierarchy, depth-first, using ORDER if given.
+ Possible values for ORDER are `:preorder` and `:postorder`, defaults to the
+ former. When reaching the end, returns a distinguished loc detectable via
+ `treepy-end-p`. If already at the end, stays there.
+
+- `treepy-next` loc &optional order
+
+ Moves to the previous LOC in the hierarchy, depth-first, using ORDER if
given.
+ Possible values for ORDER are `:preorder` and `:postorder`, defaults to the
+ former.
+
+- `treepy-end-p` loc
+
+ Returns `t` if LOC represents the end of a depth-first walk, `nil` otherwise.
+
+## Prior Art
+
+- [xiongtx/zipper.el](https://github.com/xiongtx/zipper.el): Non-generic,
EIEIO,
+ zipper implementation.
+
+- [danielfm/cl-zipper](https://github.com/danielfm/cl-zipper): Common Lisp
+ zipper implementation.
+
+## LICENSE
+
+© 2017 Daniel Barreto
+
+Distributed under the terms of the GNU GENERAL PUBLIC LICENSE, version 3.
diff --git a/treepy.el b/treepy.el
index 045245db59..b565672acc 100644
--- a/treepy.el
+++ b/treepy.el
@@ -43,9 +43,9 @@
(defun treepy-walk (inner outer form)
"Using INNER and OUTER, traverse FORM, an arbitrary data structure.
-INNER and OUTER are functions. Apply INNER to each element of
-form, building up a data structure of the same type, then apply
-OUTER to the result. Recognize cons, lists, alists, vectors and
+INNER and OUTER are functions. Apply INNER to each element of
+FORM, building up a data structure of the same type, then apply
+OUTER to the result. Recognize cons, lists, alists, vectors and
hash tables."
(cond
((and (listp form) (cdr form) (atom (cdr form))) (funcall outer (cons
(funcall inner (car form))
@@ -58,7 +58,7 @@ hash tables."
(defun treepy-postwalk (f form)
"Perform a depth-first, post-order traversal of F applied to FORM.
Call F on each sub-form, use F's return value in place of the
-original. Recognize cons, lists, alists, vectors and
+original. Recognize cons, lists, alists, vectors and
hash tables."
(treepy-walk (apply-partially #'treepy-postwalk f) f form))
@@ -67,7 +67,7 @@ hash tables."
(treepy-walk (apply-partially #'treepy-prewalk f) #'identity (funcall f
form)))
(defun treepy-postwalk-demo (form)
- "Demonstrates the behavior of `treepy-postwalk' for FORM.
+ "Demonstrate the behavior of `treepy-postwalk' for FORM.
Return a list of each form as it is walked."
(let ((walk nil))
(treepy-postwalk (lambda (x) (push x walk) x)
@@ -82,18 +82,20 @@ Return a list of each form as it is walked."
form)
(reverse walk)))
-(defun treepy-postwalk-replace (smap form)
+(defun treepy-postwalk-replace (smap form &optional testfn)
"Recursively use SMAP to transform FORM by doing replacing operations.
-Replace in FORM keys in SMAP with their values. Does replacement
-at the leaves of the tree first."
- (treepy-postwalk (lambda (x) (if (map-contains-key smap x) (map-elt smap x)
x))
+The optional TESTFN parameter is the function to be used by
+`map-contains-key'. Replace in FORM keys in SMAP with their
+values. Does replacement at the leaves of the tree first."
+ (treepy-postwalk (lambda (x) (if (map-contains-key smap x testfn) (map-elt
smap x) x))
form))
-(defun treepy-prewalk-replace (smap form)
+(defun treepy-prewalk-replace (smap form &optional testfn)
"Recursively use SMAP to transform FORM by doing replacing operations.
-Replace in FORM keys in SMAP with their values. Does replacement
-at the root of the tree first."
- (treepy-prewalk (lambda (x) (if (map-contains-key smap x) (map-elt smap x)
x))
+The optional TESTFN parameter is the function to be used by
+`map-contains-key'. Replace in FORM keys in SMAP with their
+values. Does replacement at the root of the tree first."
+ (treepy-prewalk (lambda (x) (if (map-contains-key smap x testfn) (map-elt
smap x) x))
form))
@@ -187,7 +189,7 @@ ROOT is the root node."
(defun treepy-vector-zip (root)
"Return a zipper for nested vectors, given a ROOT vector."
- (let ((make-node (lambda (node children) (apply #'vector children))) ;
(treepy--with-meta children (treepy--meta node))
+ (let ((make-node (lambda (node children) (apply #'vector children)))
(children (lambda (cs) (seq-into cs 'list))))
(treepy-zipper #'vectorp children make-node root)))
@@ -202,7 +204,7 @@ ROOT is the root node."
(funcall (treepy--meta loc ':branchp) (treepy-node loc)))
(defun treepy-children (loc)
- "Return a list of the children of node at LOC, which must be a branch."
+ "Return a children list of the node at LOC, which must be a branch."
(if (treepy-branch-p loc)
(funcall (treepy--meta loc ':children) (treepy-node loc))
(error "Called children on a leaf node")))
@@ -343,7 +345,7 @@ The LOC is only used to supply the constructor."
(treepy--meta loc)))))
(defun treepy-replace (loc node)
- "Replace in this LOC the NODE, without moving."
+ "Replace the node in this LOC with the given NODE, without moving."
(let ((context (treepy--context loc)))
(treepy--with-meta
(cons node
@@ -386,18 +388,18 @@ walk."
;; Enumeration
-
(defun treepy--preorder-next (loc)
"Move to the next LOC in the hierarchy, depth-first in preorder.
When reaching the end, returns a distinguished loc detectable via
-end?. If already at the end, stays there."
+`treepy-end-p'. If already at the end, stays there."
(if (equal :end (treepy--context loc))
loc
(let ((cloc loc))
(or
(and (treepy-branch-p cloc) (treepy-down cloc))
(treepy-right cloc)
- (let ((p cloc))
+ (let ((p cloc)
+ (pr nil))
(while (and (treepy-up p) (not (setq pr (treepy-right (treepy-up
p)))))
(setq p (treepy-up p)))
(or pr (cons (cons (treepy-node p) ':end) nil)))))))
@@ -405,7 +407,7 @@ end?. If already at the end, stays there."
(defun treepy--postorder-next (loc)
"Move to the next LOC in the hierarchy, depth-first in postorder.
When reaching the end, returns a distinguished loc detectable via
-end?. If already at the end, stays there."
+`treepy-end-p'. If already at the end, stays there."
(if (equal :end (treepy--context loc))
loc
(if (null (treepy-up loc))
@@ -445,7 +447,7 @@ If already at the root, returns nil."
(treepy-left loc))))
(defun treepy-prev (loc &optional order)
- "Move to the previous LOC in the hierarchy, depth-first. using ORDER if
given.
+ "Move to the previous LOC in the hierarchy, depth-first, using ORDER if
given.
Possible values for ORDER are `:preorder' and `:postorder',
defaults to the former."
(cl-case (or order ':preorder)