1309 lines
43 KiB
Text
1309 lines
43 KiB
Text
|
|
━━━━━━━━━
|
|||
|
|
TAXY.EL
|
|||
|
|
━━━━━━━━━
|
|||
|
|
|
|||
|
|
|
|||
|
|
[https://elpa.gnu.org/packages/taxy.svg]
|
|||
|
|
|
|||
|
|
/Now, where did I put that…/
|
|||
|
|
|
|||
|
|
This library provides a programmable way to classify arbitrary objects
|
|||
|
|
into a hierarchical taxonomy. (That's a lot of fancy words to say that
|
|||
|
|
this lets you automatically put things in nested groups.)
|
|||
|
|
|
|||
|
|
Helpful features include:
|
|||
|
|
|
|||
|
|
Dynamic taxonomies
|
|||
|
|
Objects may be classified into hierarchies automatically defined
|
|||
|
|
at runtime based on their attributes.
|
|||
|
|
Reusable taxonomies
|
|||
|
|
Taxonomy definitions may be stored in variables and reused in
|
|||
|
|
other taxonomies' descendant groups.
|
|||
|
|
Classification domain-specific language
|
|||
|
|
Easily define a custom DSL used to classify items dynamically
|
|||
|
|
(which can be extended by users).
|
|||
|
|
Flexible table view
|
|||
|
|
Based on `magit-section', with easily defined columns (also
|
|||
|
|
extendable by users).
|
|||
|
|
|
|||
|
|
|
|||
|
|
[https://elpa.gnu.org/packages/taxy.svg]
|
|||
|
|
<https://elpa.gnu.org/packages/taxy.html>
|
|||
|
|
|
|||
|
|
|
|||
|
|
1 Examples
|
|||
|
|
══════════
|
|||
|
|
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
|
|||
|
|
May these examples help you classify your understanding.
|
|||
|
|
|
|||
|
|
|
|||
|
|
1.1 Numbery (starting basically)
|
|||
|
|
────────────────────────────────
|
|||
|
|
|
|||
|
|
Let's imagine a silly taxonomy of numbers below 100:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Numbery" "A silly taxonomy of numbers."
|
|||
|
|
│ (("< 10" "Numbers below 10"
|
|||
|
|
│ ;; These numbers are leftovers from the sub-taxys below.
|
|||
|
|
│ (0 2 4 6 8)
|
|||
|
|
│ ;; These sub-taxys further classify the numbers below 10 into odd
|
|||
|
|
│ ;; and even. The odd taxy "consumes" numbers, while the even one
|
|||
|
|
│ ;; doesn't, leaving them to reappear in the parent taxy's items.
|
|||
|
|
│ (("Odd" "(consuming)"
|
|||
|
|
│ (1 3 5 7 9))
|
|||
|
|
│ ("Even" "(non-consuming)"
|
|||
|
|
│ (0 2 4 6 8))))
|
|||
|
|
│ (">= 10" "Numbers above 9"
|
|||
|
|
│ ;; Like in the "< 10" taxy, these numbers are leftovers from this
|
|||
|
|
│ ;; taxy's sub-taxys, three of which are non-consuming.
|
|||
|
|
│ (10 11 13 14 17 19 22 23 25 26 29 31 34 35 37 38 41 43 46 47 49 50 53 55 58
|
|||
|
|
│ 59 61 62 65 67 70 71 73 74 77 79 82 83 85 86 89 91 94 95 97 98)
|
|||
|
|
│ (("Divisible by 3" "(non-consuming)"
|
|||
|
|
│ (12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84
|
|||
|
|
│ 87 90 93 96 99))
|
|||
|
|
│ ("Divisible by 4" "(non-consuming)"
|
|||
|
|
│ (12 16 20 24 28 32 36 40 44 48 52 56 60 64 68 72 76 80 84 88 92 96))
|
|||
|
|
│ ("Divisible by 3 or 4" "(consuming)"
|
|||
|
|
│ ;; This taxy consumes numbers it takes in, but since these
|
|||
|
|
│ ;; numbers have already been taken in (without being consumed) by
|
|||
|
|
│ ;; the previous two sibling taxys, they may also appear in them.
|
|||
|
|
│ (12 15 16 18 20 21 24 27 28 30 32 33 36 39 40 42 44 45 48 51 52 54 56 57 60
|
|||
|
|
│ 63 64 66 68 69 72 75 76 78 80 81 84 87 88 90 92 93 96 99))
|
|||
|
|
│ ("Divisible by 5" "(non-consuming)"
|
|||
|
|
│ (10 25 35 50 55 65 70 85 95))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
You might think about how to produce that by writing some imperative
|
|||
|
|
code, but `taxy' allows you to do so in a more declarative and
|
|||
|
|
functional manner:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (require 'taxy)
|
|||
|
|
│
|
|||
|
|
│ (defvar numbery
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Numbery"
|
|||
|
|
│ :description "A silly taxonomy of numbers."
|
|||
|
|
│ :taxys (list (make-taxy
|
|||
|
|
│ :name "< 10"
|
|||
|
|
│ :description "Numbers below 10 (consuming)"
|
|||
|
|
│ :predicate (lambda (n) (< n 10))
|
|||
|
|
│ :taxys (list
|
|||
|
|
│ ;; These sub-taxys further classify the numbers below 10 into odd
|
|||
|
|
│ ;; and even. The odd taxy "consumes" numbers, while the even one
|
|||
|
|
│ ;; doesn't, leaving them to reappear in the parent taxy's items.
|
|||
|
|
│ (make-taxy :name "Odd"
|
|||
|
|
│ :description "(consuming)"
|
|||
|
|
│ :predicate #'oddp)
|
|||
|
|
│ (make-taxy :name "Even"
|
|||
|
|
│ :description "(non-consuming)"
|
|||
|
|
│ :predicate #'evenp
|
|||
|
|
│ :then #'identity)))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name ">= 10"
|
|||
|
|
│ :description "Numbers above 9 (consuming)"
|
|||
|
|
│ :predicate (lambda (n) (>= n 10))
|
|||
|
|
│ :taxys (list
|
|||
|
|
│ ;; Like in the "< 10" taxy, these sub-taxys further classify
|
|||
|
|
│ ;; the numbers, but only one of them consumes numbers it
|
|||
|
|
│ ;; takes in, leaving the rest to reappear in the parent taxy.
|
|||
|
|
│ (make-taxy :name "Divisible by 3"
|
|||
|
|
│ :description "(non-consuming)"
|
|||
|
|
│ :predicate (lambda (n) (zerop (mod n 3)))
|
|||
|
|
│ :then #'identity)
|
|||
|
|
│ (make-taxy :name "Divisible by 4"
|
|||
|
|
│ :description "(non-consuming)"
|
|||
|
|
│ :predicate (lambda (n) (zerop (mod n 4)))
|
|||
|
|
│ :then #'identity)
|
|||
|
|
│ (make-taxy :name "Divisible by 3 or 4"
|
|||
|
|
│ :description "(consuming)"
|
|||
|
|
│ ;; Since this taxy's `:then' function is unset,
|
|||
|
|
│ ;; it defaults to `ignore', which causes it to
|
|||
|
|
│ ;; consume numbers it takes in. Since these
|
|||
|
|
│ ;; numbers have already been taken in (without
|
|||
|
|
│ ;; being consumed) by the previous two sibling
|
|||
|
|
│ ;; taxys, they also appear in them.
|
|||
|
|
│ :predicate (lambda (n) (or (zerop (mod n 3))
|
|||
|
|
│ (zerop (mod n 4)))))
|
|||
|
|
│ (make-taxy :name "Divisible by 5"
|
|||
|
|
│ :description "(non-consuming)"
|
|||
|
|
│ :predicate (lambda (n) (zerop (mod n 5)))
|
|||
|
|
│ :then #'identity))))))
|
|||
|
|
│
|
|||
|
|
│ (let ((numbers (cl-loop for i below 100 collect i))
|
|||
|
|
│ ;; Since `numbery' is stored in a variable, we use an emptied
|
|||
|
|
│ ;; copy of it to avoid mutating the original taxy.
|
|||
|
|
│ (taxy (taxy-emptied numbery)))
|
|||
|
|
│ (taxy-plain (taxy-fill (reverse numbers) taxy)))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
The `taxy-fill' function applies the numbers in a "cascade" down the
|
|||
|
|
hierarchy of "taxys", and the `taxy-plain' function returns a
|
|||
|
|
meaningful subset of the taxys' slots, suitable for display.
|
|||
|
|
|
|||
|
|
|
|||
|
|
1.2 Lettery (filling incrementally)
|
|||
|
|
───────────────────────────────────
|
|||
|
|
|
|||
|
|
You can also add more items after the hierarchy has been filled. In
|
|||
|
|
this example we'll make a comprehensive taxonomy of letters. The
|
|||
|
|
first sub-taxy collects vowels, and the second, by leaving its
|
|||
|
|
predicate at the default value, `identity', collects all letters not
|
|||
|
|
collected by the first taxy, i.e. non-vowels.
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar lettery
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Lettery"
|
|||
|
|
│ :description "A comprehensive taxonomy of letters."
|
|||
|
|
│ :taxys (list (make-taxy
|
|||
|
|
│ :name "Vowels"
|
|||
|
|
│ :description "You know what those are."
|
|||
|
|
│ :predicate (lambda (l)
|
|||
|
|
│ (member-ignore-case l '("a" "e" "i" "o" "u"))))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Consonants"
|
|||
|
|
│ :description "Well, if they aren't vowels..."))))
|
|||
|
|
│
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-fill (reverse
|
|||
|
|
│ (cl-loop for l from ?a to ?n
|
|||
|
|
│ collect (upcase (char-to-string l))))
|
|||
|
|
│ lettery))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That produces:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Lettery" "A comprehensive taxonomy of letters."
|
|||
|
|
│ (("Vowels" "You know what those are."
|
|||
|
|
│ ("A" "E" "I"))
|
|||
|
|
│ ("Consonants" "Well, if they aren't vowels..."
|
|||
|
|
│ ("B" "C" "D" "F" "G" "H" "J" "K" "L" "M" "N"))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Oops, we forgot the letters after N! Let's add them, too:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-fill (reverse
|
|||
|
|
│ (cl-loop for l from ?n to ?z
|
|||
|
|
│ collect (upcase (char-to-string l))))
|
|||
|
|
│ lettery))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Which gives us:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Lettery" "A comprehensive taxonomy of letters."
|
|||
|
|
│ (("Vowels" "You know what those are."
|
|||
|
|
│ ("O" "U" "A" "E" "I"))
|
|||
|
|
│ ("Consonants" "Well, if they aren't vowels..."
|
|||
|
|
│ ("N" "P" "Q" "R" "S" "T" "V" "W" "X" "Y" "Z" "B" "C" "D" "F" "G" "H" "J" "K" "L" "M" "N"))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Oh, they're out of order, now. That won't do. Let's fix that:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-sort #'string< #'identity lettery))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That's better:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Lettery" "A comprehensive taxonomy of letters."
|
|||
|
|
│ (("Vowels" "You know what those are."
|
|||
|
|
│ ("A" "E" "I" "O" "U"))
|
|||
|
|
│ ("Consonants" "Well, if they aren't vowels..."
|
|||
|
|
│ ("B" "C" "D" "F" "G" "H" "J" "K" "L" "M" "N" "N" "P" "Q" "R" "S" "T" "V" "W" "X" "Y" "Z"))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
1.3 Sporty (understanding completely)
|
|||
|
|
─────────────────────────────────────
|
|||
|
|
|
|||
|
|
Let's try to understand a few things about sports. First we'll define
|
|||
|
|
a struct to make them easier to grasp:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (cl-defstruct sport
|
|||
|
|
│ name uses venue fun)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Now we'll make a list of sports:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar sports
|
|||
|
|
│ (list (make-sport :name "Baseball"
|
|||
|
|
│ :uses '(bat ball glove)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Football"
|
|||
|
|
│ :uses '(ball)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Basketball"
|
|||
|
|
│ :uses '(ball hoop)
|
|||
|
|
│ :venue 'indoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Tennis"
|
|||
|
|
│ :uses '(ball racket)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Racquetball"
|
|||
|
|
│ :uses '(ball racket)
|
|||
|
|
│ :venue 'indoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Handball"
|
|||
|
|
│ :uses '(ball glove)
|
|||
|
|
│ :venue 'indoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Soccer"
|
|||
|
|
│ :uses '(ball)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun nil)
|
|||
|
|
│ (make-sport :name "Disc golf"
|
|||
|
|
│ :uses '(disc basket)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Ultimate"
|
|||
|
|
│ :uses '(disc)
|
|||
|
|
│ :venue 'outdoor
|
|||
|
|
│ :fun t)
|
|||
|
|
│ (make-sport :name "Volleyball"
|
|||
|
|
│ :uses '(ball)
|
|||
|
|
│ :venue 'indoor
|
|||
|
|
│ :fun t)))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
And finally we'll define a taxy to organize them. In this, we use a
|
|||
|
|
helper macro to make the `member' function easier to use in the list
|
|||
|
|
of key functions:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar sporty
|
|||
|
|
│ (cl-macrolet ((in (needle haystack)
|
|||
|
|
│ `(lambda (item)
|
|||
|
|
│ (when (member ,needle (funcall ,haystack item))
|
|||
|
|
│ ,needle))))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Sporty"
|
|||
|
|
│ :take (lambda (item taxy)
|
|||
|
|
│ (taxy-take-keyed
|
|||
|
|
│ (list #'sport-venue
|
|||
|
|
│ (in 'ball 'sport-uses)
|
|||
|
|
│ (in 'disc 'sport-uses)
|
|||
|
|
│ (in 'glove 'sport-uses)
|
|||
|
|
│ (in 'racket 'sport-uses))
|
|||
|
|
│ item taxy
|
|||
|
|
│ ;; We set the `:then' function of the taxys
|
|||
|
|
│ ;; created by `taxy-take-keyed' to `identity'
|
|||
|
|
│ ;; so they will not consume their items.
|
|||
|
|
│ :then #'identity)))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Now let's fill the taxy with the sports and format it:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (thread-last sporty
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ((("Sporty"
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ((ball
|
|||
|
|
│ ("Volleyball" "Basketball")
|
|||
|
|
│ ((glove
|
|||
|
|
│ ("Handball"))
|
|||
|
|
│ (racket
|
|||
|
|
│ ("Racquetball"))))))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ((disc
|
|||
|
|
│ ("Ultimate" "Disc golf"))
|
|||
|
|
│ (ball
|
|||
|
|
│ ("Soccer" "Football")
|
|||
|
|
│ ((racket
|
|||
|
|
│ ("Tennis"))
|
|||
|
|
│ (glove
|
|||
|
|
│ ("Baseball"))))))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That's pretty sporty. But classifying them by venue first makes the
|
|||
|
|
racket and glove sports not be listed together. Let's swap the key
|
|||
|
|
functions around so the venue is classified at the deepest level of
|
|||
|
|
the hierarchy:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar sporty
|
|||
|
|
│ (cl-macrolet ((in (needle haystack)
|
|||
|
|
│ `(lambda (item)
|
|||
|
|
│ (when (member ,needle (funcall ,haystack item))
|
|||
|
|
│ ,needle))))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Sporty"
|
|||
|
|
│ :take (lambda (item taxy)
|
|||
|
|
│ (taxy-take-keyed
|
|||
|
|
│ (list (in 'ball 'sport-uses)
|
|||
|
|
│ (in 'disc 'sport-uses)
|
|||
|
|
│ (in 'glove 'sport-uses)
|
|||
|
|
│ (in 'racket 'sport-uses)
|
|||
|
|
│ #'sport-venue)
|
|||
|
|
│ item taxy
|
|||
|
|
│ :then #'identity)))))
|
|||
|
|
│
|
|||
|
|
│ (thread-last sporty
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ((("Sporty"
|
|||
|
|
│ ((disc
|
|||
|
|
│ ((outdoor
|
|||
|
|
│ ("Ultimate" "Disc golf"))))
|
|||
|
|
│ (ball
|
|||
|
|
│ ((racket
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ("Racquetball"))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ("Tennis"))))
|
|||
|
|
│ (indoor
|
|||
|
|
│ ("Volleyball" "Basketball"))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ("Soccer" "Football"))
|
|||
|
|
│ (glove
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ("Handball"))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ("Baseball"))))))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That's better. But I'd also like to see a very simple classification
|
|||
|
|
to help me decide what to play:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (thread-last
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Funny"
|
|||
|
|
│ :take (lambda (item taxy)
|
|||
|
|
│ (taxy-take-keyed
|
|||
|
|
│ (list (lambda (sport)
|
|||
|
|
│ (if (sport-fun sport)
|
|||
|
|
│ 'fun 'boring))
|
|||
|
|
│ #'sport-venue)
|
|||
|
|
│ item taxy)))
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ((("Funny"
|
|||
|
|
│ ((boring
|
|||
|
|
│ ((outdoor
|
|||
|
|
│ ("Soccer"))))
|
|||
|
|
│ (fun
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ("Volleyball" "Handball" "Racquetball" "Basketball"))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ("Ultimate" "Disc golf" "Tennis" "Football" "Baseball"))))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Ah, now I understand.
|
|||
|
|
|
|||
|
|
|
|||
|
|
1.4 Applications
|
|||
|
|
────────────────
|
|||
|
|
|
|||
|
|
Some example applications may be found in the [examples directory]:
|
|||
|
|
|
|||
|
|
⁃ Deffy shows top-level definitions and forms in an Elisp project or
|
|||
|
|
file:
|
|||
|
|
⁃ Diredy rearranges a Dired buffer into groups by file size and type:
|
|||
|
|
⁃ Musicy shows a music library with tracks categorized by genre,
|
|||
|
|
artist, year, album, etc:
|
|||
|
|
|
|||
|
|
|
|||
|
|
[examples directory] <file:examples/>
|
|||
|
|
|
|||
|
|
|
|||
|
|
2 Installation
|
|||
|
|
══════════════
|
|||
|
|
|
|||
|
|
`taxy' is distributed in [GNU ELPA], which is available in Emacs by
|
|||
|
|
default. Use `M-x package-install RET taxy RET', then `(require
|
|||
|
|
'taxy)' in your Elisp project.
|
|||
|
|
|
|||
|
|
|
|||
|
|
[GNU ELPA] <https://elpa.gnu.org/>
|
|||
|
|
|
|||
|
|
|
|||
|
|
3 Usage
|
|||
|
|
═══════
|
|||
|
|
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
|
|||
|
|
A taxy is defined with the `make-taxy' constructor, like:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (make-taxy :name "Numbery"
|
|||
|
|
│ :description "A silly taxonomy of numbers."
|
|||
|
|
│ :predicate #'numberp
|
|||
|
|
│ :then #'ignore
|
|||
|
|
│ :taxys (list ...))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
The `:predicate' function determines whether an object fits into that
|
|||
|
|
taxy. If it does, `taxy-fill' adds the object to that taxy's
|
|||
|
|
descendant `:taxys', if present, or to its own `:items'. The function
|
|||
|
|
defaults to `identity', so a taxy "takes in" any object by default
|
|||
|
|
(i.e. if you only apply objects you want to classify, there's no need
|
|||
|
|
to test them at the top-level taxy).
|
|||
|
|
|
|||
|
|
The `:then' function determines what happens to an object after being
|
|||
|
|
taken in to the taxy's `:items': if the function, called with the
|
|||
|
|
object, returns a non-nil value, that value is applied to other taxys
|
|||
|
|
at the same level until one of their `:then' functions returns nil or
|
|||
|
|
no more taxys remain. The function defaults to `ignore', which makes
|
|||
|
|
a taxy "consume" its items by default. Setting the function to,
|
|||
|
|
e.g. `identity', makes it not consume them, leaving them eligible to
|
|||
|
|
also be taken into subsequent taxys, or to appear in the parent taxy's
|
|||
|
|
items.
|
|||
|
|
|
|||
|
|
After defining a taxy, call `taxy-fill' with it and a list of objects
|
|||
|
|
to fill the taxy's hierarchy. *Note:* `taxy-fill' modifies the taxy
|
|||
|
|
given to it (filling its `:items' and those of its `:taxys'), so when
|
|||
|
|
using a statically defined taxy (e.g. one defined with `defvar'), you
|
|||
|
|
should pass `taxy-fill' a taxy copied with `taxy-emptied', which
|
|||
|
|
recursively copies a taxy without `:items'.
|
|||
|
|
|
|||
|
|
To return a taxy in a more human-readable format (with only relevant
|
|||
|
|
fields included), use `taxy-plain'. You may also use `taxy-mapcar' to
|
|||
|
|
replace items in a taxy with, e.g. a more useful representation.
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.1 Reusable taxys
|
|||
|
|
──────────────────
|
|||
|
|
|
|||
|
|
Since taxys are structs, they may be stored in variables and used in
|
|||
|
|
other structs (being sure to copy the root taxy with `taxy-emptied'
|
|||
|
|
before filling). For example, this shows using `taxy' to classify
|
|||
|
|
Matrix rooms in [Ement.el]:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defun ement-roomy-buffer (room)
|
|||
|
|
│ (alist-get 'buffer (ement-room-local room)))
|
|||
|
|
│
|
|||
|
|
│ (defvar ement-roomy-unread
|
|||
|
|
│ (make-taxy :name "Unread"
|
|||
|
|
│ :predicate (lambda (room)
|
|||
|
|
│ (buffer-modified-p (ement-roomy-buffer room)))))
|
|||
|
|
│
|
|||
|
|
│ (defvar ement-roomy-opened
|
|||
|
|
│ (make-taxy :name "Opened"
|
|||
|
|
│ :description "Rooms with buffers"
|
|||
|
|
│ :predicate #'ement-roomy-buffer
|
|||
|
|
│ :taxys (list ement-roomy-unread
|
|||
|
|
│ (make-taxy))))
|
|||
|
|
│
|
|||
|
|
│ (defvar ement-roomy-closed
|
|||
|
|
│ (make-taxy :name "Closed"
|
|||
|
|
│ :description "Rooms without buffers"
|
|||
|
|
│ :predicate (lambda (room)
|
|||
|
|
│ (not (ement-roomy-buffer room)))))
|
|||
|
|
│
|
|||
|
|
│ (defvar ement-roomy
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Ement Rooms"
|
|||
|
|
│ :taxys (list (make-taxy
|
|||
|
|
│ :name "Direct"
|
|||
|
|
│ :description "Direct messaging rooms"
|
|||
|
|
│ :predicate (lambda (room)
|
|||
|
|
│ (ement-room--direct-p room ement-session))
|
|||
|
|
│ :taxys (list ement-roomy-opened
|
|||
|
|
│ ement-roomy-closed))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Non-direct"
|
|||
|
|
│ :description "Group chat rooms"
|
|||
|
|
│ :taxys (list ement-roomy-opened
|
|||
|
|
│ ement-roomy-closed)))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Note how the taxys defined in the first three variables are used in
|
|||
|
|
subsequent taxys. As well, the `ement-roomy-opened' taxy has an
|
|||
|
|
"anonymous" taxy, which collects any rooms that aren't collected by
|
|||
|
|
its sibling taxy (otherwise those objects would be collected into the
|
|||
|
|
parent, "Opened" taxy, which may not always be the most useful way to
|
|||
|
|
present the objects).
|
|||
|
|
|
|||
|
|
Using those defined taxys, we then fill the `ement-roomy' taxy with
|
|||
|
|
all of the rooms in the user's session, and then use `taxy-mapcar' to
|
|||
|
|
replace the room structs with useful representations for display:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-mapcar (lambda (room)
|
|||
|
|
│ (list (ement-room--room-display-name room)
|
|||
|
|
│ (ement-room-id room)))
|
|||
|
|
│ (taxy-fill (ement-session-rooms ement-session)
|
|||
|
|
│ (taxy-emptied ement-roomy))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
This produces:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Ement Rooms"
|
|||
|
|
│ (("Direct" "Direct messaging rooms"
|
|||
|
|
│ (("Opened" "Rooms with buffers"
|
|||
|
|
│ (("Unread"
|
|||
|
|
│ (("Lars Ingebrigtsen" "!nope:gnus.org")))))
|
|||
|
|
│ ("Closed" "Rooms without buffers"
|
|||
|
|
│ (("John Wiegley" "!not-really:newartisans.com")
|
|||
|
|
│ ("Eli Zaretskii" "!im-afraid-not:gnu.org")))))
|
|||
|
|
│ ("Non-direct" "Group chat rooms"
|
|||
|
|
│ (("Opened" "Rooms with buffers"
|
|||
|
|
│ (("Unread"
|
|||
|
|
│ (("Emacs" "!WfZsmtnxbxTdoYPkaT:greyface.org")
|
|||
|
|
│ ("#emacs" "!KuaCUVGoCiunYyKEpm:libera.chat")))
|
|||
|
|
│ ;; The non-unread buffers in the "anonymous" taxy.
|
|||
|
|
│ ((("magit/magit" "!HZYimOcmEAsAxOcgpE:gitter.im")
|
|||
|
|
│ ("Ement.el" "!NicAJNwJawmHrEhqZs:matrix.org")
|
|||
|
|
│ ("#emacsconf" "!UjTTDnYmSAslLTtMCF:libera.chat")
|
|||
|
|
│ ("Emacs Matrix Client" "!ZrZoyXEyFrzcBZKNis:matrix.org")
|
|||
|
|
│ ("org-mode" "!rUhEinythPhVTdddsb:matrix.org")
|
|||
|
|
│ ("This Week in Matrix (TWIM)" "!xYvNcQPhnkrdUmYczI:matrix.org")))))
|
|||
|
|
│ ("Closed" "Rooms without buffers"
|
|||
|
|
│ (("#matrix-spec" "!NasysSDfxKxZBzJJoE:matrix.org")
|
|||
|
|
│ ("#commonlisp" "!IiGsrmKRHzpupHRaKS:libera.chat")
|
|||
|
|
│ ("Matrix HQ" "!OGEhHVWSdvArJzumhm:matrix.org")
|
|||
|
|
│ ("#lisp" "!czLxhhEegTEGNKUBgo:libera.chat")
|
|||
|
|
│ ("Emacs" "!gLamGIXTWBaDFfhEeO:matrix.org")
|
|||
|
|
│ ("#matrix-dev:matrix.org" "!jxlRxnrZCsjpjDubDX:matrix.org")))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
[Ement.el] <https://github.com/alphapapa/ement.el>
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.2 Threading macros
|
|||
|
|
────────────────────
|
|||
|
|
|
|||
|
|
If you happen to like macros, `taxy' works well with threading
|
|||
|
|
(i.e. `thread-last' or `->>'):
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (thread-last ement-roomy
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill (ement-session-rooms ement-session))
|
|||
|
|
│ (taxy-mapcar (lambda (room)
|
|||
|
|
│ (list (ement-room--room-display-name room)
|
|||
|
|
│ (ement-room-id room))))
|
|||
|
|
│ taxy-plain)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.3 Modifying filled taxys
|
|||
|
|
──────────────────────────
|
|||
|
|
|
|||
|
|
Sometimes it's necessary to modify a taxy after filling it with
|
|||
|
|
objects, e.g. to sort the items and/or the sub-taxys. For this, use
|
|||
|
|
the function `taxy-mapc-taxys' (a.k.a. `taxy-mapc*'). For example, in
|
|||
|
|
the sample application [musicy.el], the taxys and their items are
|
|||
|
|
sorted after filling, like so:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defun musicy-files (files)
|
|||
|
|
│ (thread-last musicy-taxy
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill files)
|
|||
|
|
│ ;; Sort sub-taxys by their name.
|
|||
|
|
│ (taxy-sort* #'string< #'taxy-name)
|
|||
|
|
│ ;; Sort sub-taxys' items by name.
|
|||
|
|
│ (taxy-sort #'string< #'identity)
|
|||
|
|
│ taxy-magit-section-pp))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
[musicy.el] <file:examples/musicy.el>
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.4 Dynamic taxys
|
|||
|
|
─────────────────
|
|||
|
|
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
•
|
|||
|
|
|
|||
|
|
You may not always know in advance what taxonomy a set of objects fits
|
|||
|
|
into, so `taxy' lets you add taxys dynamically by using the `:take'
|
|||
|
|
function to add a taxy when an object is "taken into" a parent taxy's
|
|||
|
|
items. For example, you could dynamically classify buffers by their
|
|||
|
|
major mode like so:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defun buffery-major-mode (buffer)
|
|||
|
|
│ (buffer-local-value 'major-mode buffer))
|
|||
|
|
│
|
|||
|
|
│ (defvar buffery
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Buffers"
|
|||
|
|
│ :taxys (list
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Modes"
|
|||
|
|
│ :take (apply-partially #'taxy-take-keyed (list #'buffery-major-mode))))))
|
|||
|
|
│
|
|||
|
|
│ ;; Note the use of `taxy-emptied' to avoid mutating the original taxy definition.
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-fill (buffer-list)
|
|||
|
|
│ (taxy-emptied buffery)))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
The taxy's `:take' function is set to the `taxy-take-keyed' function,
|
|||
|
|
partially applied with the `buffery-major-mode' function as its list
|
|||
|
|
of `key-fns' (`taxy-fill' supplies the buffer and the taxy as
|
|||
|
|
arguments), and it produces this taxonomy of buffers:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Buffers"
|
|||
|
|
│ (("Modes"
|
|||
|
|
│ ((magit-process-mode
|
|||
|
|
│ (#<buffer magit-process: taxy.el> #<buffer magit-process: > #<buffer magit-process: notes>))
|
|||
|
|
│ (messages-buffer-mode
|
|||
|
|
│ (#<buffer *Messages*>))
|
|||
|
|
│ (special-mode
|
|||
|
|
│ (#<buffer *Warnings*> #<buffer *elfeed-log*>))
|
|||
|
|
│ (dired-mode
|
|||
|
|
│ (#<buffer ement.el<emacs>>))
|
|||
|
|
│ (Custom-mode
|
|||
|
|
│ (#<buffer *Customize Apropos*>))
|
|||
|
|
│ (fundamental-mode
|
|||
|
|
│ (#<buffer *helm candidates:Bookmarks*> #<buffer *Backtrace*>))
|
|||
|
|
│ (magit-diff-mode
|
|||
|
|
│ (#<buffer magit-diff: taxy.el> #<buffer magit-diff: notes> #<buffer magit-diff: ement.el>))
|
|||
|
|
│ (compilation-mode
|
|||
|
|
│ (#<buffer *compilation*> #<buffer *Compile-Log*>))
|
|||
|
|
│ (Info-mode
|
|||
|
|
│ (#<buffer *helm info temp buffer*> #<buffer *info*>))
|
|||
|
|
│ (help-mode
|
|||
|
|
│ (#<buffer *Help*>))
|
|||
|
|
│ (emacs-lisp-mode
|
|||
|
|
│ (#<buffer ement.el<ement.el>> #<buffer ement-room-list.el> #<buffer *scratch*>
|
|||
|
|
│ #<buffer ement-room.el> #<buffer init.el> #<buffer bufler.el>
|
|||
|
|
│ #<buffer dash.el> #<buffer *Pp Eval Output*> #<buffer taxy.el> #<buffer scratch.el>))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.4.1 Multi-level dynamic taxys
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
Of course, the point of taxonomies is that they aren't restricted to a
|
|||
|
|
single level of depth, so you may also use the function
|
|||
|
|
`taxy-take-keyed' to dynamically make multi-level taxys.
|
|||
|
|
|
|||
|
|
Expanding on the previous example, we use `cl-labels' to define
|
|||
|
|
functions which are used in the taxy's definition, which are used in
|
|||
|
|
the `:take' function, which calls `taxy-take-keyed' (rather than using
|
|||
|
|
`apply-partially' like in the previous example, we use a lambda
|
|||
|
|
function, which performs better than partially applied functions).
|
|||
|
|
Then when the taxy is filled, a multi-level hierarchy is created
|
|||
|
|
dynamically, organizing buffers first by their directory, and then by
|
|||
|
|
mode in each directory.
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar buffery
|
|||
|
|
│ (cl-labels ((buffer-mode (buffer) (buffer-local-value 'major-mode buffer))
|
|||
|
|
│ (buffer-directory (buffer) (buffer-local-value 'default-directory buffer)))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Buffers"
|
|||
|
|
│ :taxys (list
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Directories"
|
|||
|
|
│ :take (lambda (item taxy)
|
|||
|
|
│ (taxy-take-keyed (list #'buffer-directory #'buffer-mode) item taxy)))))))
|
|||
|
|
│
|
|||
|
|
│ (taxy-plain
|
|||
|
|
│ (taxy-fill (buffer-list)
|
|||
|
|
│ (taxy-emptied buffery)))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That produces a list like:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Buffers"
|
|||
|
|
│ (("Directories"
|
|||
|
|
│ (("~/src/emacs/ement.el/"
|
|||
|
|
│ ((dired-mode
|
|||
|
|
│ (#<buffer ement.el<emacs>))
|
|||
|
|
│ (emacs-lisp-mode
|
|||
|
|
│ (#<buffer ement.el<ement.el> #<buffer ement-room-list.el> #<buffer ement-room.el>))
|
|||
|
|
│ (magit-diff-mode
|
|||
|
|
│ (#<buffer magit-diff: ement.el>))))
|
|||
|
|
│ ("~/src/emacs/taxy.el/"
|
|||
|
|
│ ((dired-mode
|
|||
|
|
│ (#<buffer taxy.el<emacs>))
|
|||
|
|
│ (Info-mode
|
|||
|
|
│ (#<buffer *info*>))
|
|||
|
|
│ (magit-status-mode
|
|||
|
|
│ (#<buffer magit: taxy.el>))
|
|||
|
|
│ (emacs-lisp-mode
|
|||
|
|
│ (#<buffer taxy-magit-section.el> #<buffer taxy.el<taxy.el> #<buffer scratch.el>))))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.4.2 "Chains" of independent, multi-level dynamic taxys
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
/Naming things is hard./
|
|||
|
|
|
|||
|
|
Going a step further, each element in the `taxy-take-keyed' function's
|
|||
|
|
`KEY-FNS' argument may be a list of functions (or a list of lists of
|
|||
|
|
functions, etc.), which creates a "chain" of "independent" dynamic
|
|||
|
|
taxys. Each such chain may be said to "short-circuit" the filling
|
|||
|
|
process in that, when an object is "taken" by the first key function
|
|||
|
|
in a chain, the object is not "offered" to other functions outside
|
|||
|
|
that chain. This allows each dynamic sub-taxy to have its own set of
|
|||
|
|
sub-taxys, rather than sharing the same "global" set. In effect, this
|
|||
|
|
creates multiple, unique taxonomies that share a single root taxy.
|
|||
|
|
|
|||
|
|
Building on the `sporty' example, let's define a taxy in which outdoor
|
|||
|
|
sports are classified only by whether they involve a disc, but indoor
|
|||
|
|
sports are additionally classified by whatever equipment they may use:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar sporty-dynamic
|
|||
|
|
│ (cl-macrolet ((in (needle haystack)
|
|||
|
|
│ `(lambda (item)
|
|||
|
|
│ (when (member ,needle (funcall ,haystack item))
|
|||
|
|
│ ,needle))))
|
|||
|
|
│ (cl-labels ((outdoor-p
|
|||
|
|
│ (sport) (when (eq 'outdoor (sport-venue sport))
|
|||
|
|
│ "Outdoor"))
|
|||
|
|
│ (indoor-p
|
|||
|
|
│ (sport) (when (eq 'indoor (sport-venue sport))
|
|||
|
|
│ "Indoor"))
|
|||
|
|
│ (disc-p
|
|||
|
|
│ (sport) (if (funcall (in 'disc 'sport-uses) sport)
|
|||
|
|
│ 'disc
|
|||
|
|
│ 'non-disc)))
|
|||
|
|
│ (make-taxy
|
|||
|
|
│ :name "Sporty (dynamic)"
|
|||
|
|
│ :take (lambda (item taxy)
|
|||
|
|
│ (taxy-take-keyed
|
|||
|
|
│ (list (list #'outdoor-p #'disc-p)
|
|||
|
|
│ (list #'indoor-p
|
|||
|
|
│ (in 'ball 'sport-uses)
|
|||
|
|
│ (in 'disc 'sport-uses)
|
|||
|
|
│ (in 'glove 'sport-uses)
|
|||
|
|
│ (in 'racket 'sport-uses)))
|
|||
|
|
│ item taxy))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Now let's fill the taxy with the sports and format it:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (thread-last sporty-dynamic
|
|||
|
|
│ taxy-emptied
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Sporty (dynamic)"
|
|||
|
|
│ (("Indoor"
|
|||
|
|
│ ((ball
|
|||
|
|
│ ("Volleyball" "Basketball")
|
|||
|
|
│ ((glove
|
|||
|
|
│ ("Handball"))
|
|||
|
|
│ (racket
|
|||
|
|
│ ("Racquetball"))))))
|
|||
|
|
│ ("Outdoor"
|
|||
|
|
│ ((disc
|
|||
|
|
│ ("Ultimate" "Disc golf"))
|
|||
|
|
│ (non-disc
|
|||
|
|
│ ("Soccer" "Tennis" "Football" "Baseball"))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.4.3 Defining a classification domain-specific language
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
When writing a larger Taxy-based application, it may be necessary to
|
|||
|
|
define a number of key functions that would be unwieldy to manage in a
|
|||
|
|
`cl-labels' form. For this case, Taxy provides the macro
|
|||
|
|
`taxy-define-key-definer' to easily define Taxy key functions in an
|
|||
|
|
application library. Those functions are then passed to the function
|
|||
|
|
`taxy-make-take-function' at runtime, along with a list of keys being
|
|||
|
|
used to classify items. Using these allows key functions to be
|
|||
|
|
defined in top-level forms, and it allows an application to be
|
|||
|
|
extended by users by defining additional key functions in their
|
|||
|
|
configurations.
|
|||
|
|
|
|||
|
|
Extending the previous `sporty' example, let's redefine its key
|
|||
|
|
functions using `taxy-define-key-definer':
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (taxy-define-key-definer sporty-define-key
|
|||
|
|
│ sporty-keys "sporty"
|
|||
|
|
│ "Define a `sporty' key function.")
|
|||
|
|
│
|
|||
|
|
│ (sporty-define-key disc-based ()
|
|||
|
|
│ (if (member 'disc (sport-uses item))
|
|||
|
|
│ "Disc-based"
|
|||
|
|
│ "Non-disc-based"))
|
|||
|
|
│
|
|||
|
|
│ (sporty-define-key uses (&optional thing)
|
|||
|
|
│ (pcase thing
|
|||
|
|
│ (`nil (sport-uses item))
|
|||
|
|
│ (_ (when (cl-typecase (sport-uses item)
|
|||
|
|
│ (symbol (equal thing (sport-uses item)))
|
|||
|
|
│ (list (member thing (sport-uses item))))
|
|||
|
|
│ thing))))
|
|||
|
|
│
|
|||
|
|
│ (sporty-define-key venue (&optional place)
|
|||
|
|
│ (pcase place
|
|||
|
|
│ (`nil (sport-venue item))
|
|||
|
|
│ (_ (when (equal place (sport-venue item))
|
|||
|
|
│ (sport-venue item)))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Now we'll define the default keys to use when classifying items. This
|
|||
|
|
list is equivalent to the one passed to `taxy-take-keyed' in the
|
|||
|
|
previous, "Chains" example.
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (defvar sporty-default-keys
|
|||
|
|
│ '(
|
|||
|
|
│ ((venue 'outdoor)
|
|||
|
|
│ disc-based)
|
|||
|
|
│
|
|||
|
|
│ ((venue 'indoor)
|
|||
|
|
│ (uses 'ball)
|
|||
|
|
│ (uses 'disc)
|
|||
|
|
│ (uses 'glove)
|
|||
|
|
│ (uses 'racket))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Finally, rather than using a pre-made taxy struct, we make one at
|
|||
|
|
runtime, making the `:take' function with `taxy-make-take-function'.
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (let ((taxy (make-taxy
|
|||
|
|
│ :name "Sporty (DSL)"
|
|||
|
|
│ :take (taxy-make-take-function sporty-default-keys
|
|||
|
|
│ sporty-keys))))
|
|||
|
|
│ (thread-last taxy
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
Which gives us:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Sporty (DSL)"
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ((ball
|
|||
|
|
│ ("Volleyball" "Basketball")
|
|||
|
|
│ ((glove
|
|||
|
|
│ ("Handball"))
|
|||
|
|
│ (racket
|
|||
|
|
│ ("Racquetball"))))))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ (("Disc-based"
|
|||
|
|
│ ("Ultimate" "Disc golf"))
|
|||
|
|
│ ("Non-disc-based"
|
|||
|
|
│ ("Soccer" "Tennis" "Football" "Baseball"))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
As you can see, the result is the same as that in the previous
|
|||
|
|
example, but we've defined a kind of DSL for grouping sports in a
|
|||
|
|
modular, extendable way.
|
|||
|
|
|
|||
|
|
This also allows the grouping keys to be easily changed at runtime,
|
|||
|
|
producing a different result. For example, we could group sports by,
|
|||
|
|
first, whether they use a ball, and then by venue. Let's do this in a
|
|||
|
|
function so that users can pass their own list of keys:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (cl-defun sporty-classify (sports &key (keys sporty-default-keys))
|
|||
|
|
│ (declare (indent defun))
|
|||
|
|
│ (let* ((taxy (make-taxy
|
|||
|
|
│ :name "Sporty (DSL)"
|
|||
|
|
│ :take (taxy-make-take-function keys
|
|||
|
|
│ sporty-keys))))
|
|||
|
|
│ (thread-last taxy
|
|||
|
|
│ (taxy-fill sports)
|
|||
|
|
│ (taxy-mapcar #'sport-name)
|
|||
|
|
│ taxy-plain)))
|
|||
|
|
│
|
|||
|
|
│ (sporty-classify sports
|
|||
|
|
│ :keys '((uses 'ball) venue))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
And this produces:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ ("Sporty (DSL)"
|
|||
|
|
│ ((outdoor
|
|||
|
|
│ ("Ultimate" "Disc golf"))
|
|||
|
|
│ (ball
|
|||
|
|
│ ((indoor
|
|||
|
|
│ ("Volleyball" "Handball" "Racquetball" "Basketball"))
|
|||
|
|
│ (outdoor
|
|||
|
|
│ ("Soccer" "Tennis" "Football" "Baseball"))))))
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.5 Magit section
|
|||
|
|
─────────────────
|
|||
|
|
|
|||
|
|
Showing a `taxy' with `magit-section' is easy using the library
|
|||
|
|
[taxy-magit-section], which is packaged separately:
|
|||
|
|
|
|||
|
|
┌────
|
|||
|
|
│ (require 'taxy-magit-section)
|
|||
|
|
│
|
|||
|
|
│ ;; Using the `numbery' taxy defined in earlier examples:
|
|||
|
|
│ (thread-last numbery
|
|||
|
|
│ taxy-emptied ;; Get an empty copy of the taxy, since it's defined in a variable.
|
|||
|
|
│ (taxy-fill (reverse (cl-loop for i below 30 collect i)))
|
|||
|
|
│ taxy-magit-section-pp)
|
|||
|
|
└────
|
|||
|
|
|
|||
|
|
That shows a buffer like this:
|
|||
|
|
|
|||
|
|
|
|||
|
|
[taxy-magit-section]
|
|||
|
|
<https://github.com/alphapapa/taxy.el/tree/package/taxy-magit-section>
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.6 Reference
|
|||
|
|
─────────────
|
|||
|
|
|
|||
|
|
In Emacs 28+, see also `M-x shortdoc-display-group RET taxy RET'.
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.6.1 Functions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
taxy-flatten (taxy)
|
|||
|
|
Return a list of items in `taxy' and its sub-taxys.
|
|||
|
|
|
|||
|
|
taxy-emptied (taxy)
|
|||
|
|
Return a copy of `taxy' without items. Omits =taxy=’s items and
|
|||
|
|
those of its descendant taxys. Useful when reusing taxy
|
|||
|
|
definitions.
|
|||
|
|
|
|||
|
|
taxy-fill (items taxy)
|
|||
|
|
Fill `taxy' with `items' according to its definition.
|
|||
|
|
|
|||
|
|
taxy-make-take-function (keys aliases)
|
|||
|
|
Return a taxy "take" function for `keys'. Each of `keys' should
|
|||
|
|
be a function alias defined in `aliases', or a list of such
|
|||
|
|
`key-fns' (recursively, ad infinitum, approximately). `aliases'
|
|||
|
|
should be an alist mapping aliases to functions (such as defined
|
|||
|
|
with a definer defined by `taxy-define-key-definer').
|
|||
|
|
|
|||
|
|
taxy-mapc-taxys (fn taxy)
|
|||
|
|
*Alias:* `taxy-mapc*'
|
|||
|
|
|
|||
|
|
Return `taxy' having applied `fn' to it and its descendants.
|
|||
|
|
Does not copy `taxy'. Destructively modifies `taxy', if `fn'
|
|||
|
|
does.
|
|||
|
|
|
|||
|
|
taxy-mapcar-items (fn taxy)
|
|||
|
|
*Alias:* `taxy-mapcar'
|
|||
|
|
|
|||
|
|
Return copy of `taxy', having replaced its items with the value
|
|||
|
|
of `fn' on each. Replaces every item in `taxy' and its
|
|||
|
|
descendants. Useful to replace items with a more useful form
|
|||
|
|
after classification.
|
|||
|
|
|
|||
|
|
taxy-plain (taxy)
|
|||
|
|
Return a list of the human-readable parts of `taxy'.
|
|||
|
|
|
|||
|
|
taxy-size (taxy)
|
|||
|
|
Return the number of items `taxy' holds. Includes items in
|
|||
|
|
`taxy' ’s sub-taxys.
|
|||
|
|
|
|||
|
|
taxy-sort-items (pred key taxy)
|
|||
|
|
*Alias:* `taxy-sort'
|
|||
|
|
|
|||
|
|
Sort `taxy' ’s items by `pred' and `key'. Sorts items in `taxy'
|
|||
|
|
and its sub-taxys. `key' is passed to `cl-sort', which see.
|
|||
|
|
|
|||
|
|
taxy-sort-taxys (pred key taxy)
|
|||
|
|
*Alias:* `taxy-sort*'
|
|||
|
|
|
|||
|
|
Sort `taxy' ’s sub-taxys by `pred' and `key'. `key' is passed
|
|||
|
|
to `cl-sort', which see.
|
|||
|
|
|
|||
|
|
|
|||
|
|
3.6.2 Macros
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
taxy-define-key-definer (name variable prefix docstring)
|
|||
|
|
Define a macro `name' that defines a key-function-defining
|
|||
|
|
macro. The defined macro, having string `docstring', associates
|
|||
|
|
the defined key functions with their aliases in an alist stored
|
|||
|
|
in symbol `variable'. The defined key functions are named
|
|||
|
|
having string `prefix', which will have a hyphen appended to it.
|
|||
|
|
The key functions take one or more arguments, the first of which
|
|||
|
|
is the item being tested, bound within the function to `item'.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4 Changelog
|
|||
|
|
═══════════
|
|||
|
|
|
|||
|
|
4.1 0.10.2
|
|||
|
|
──────────
|
|||
|
|
|
|||
|
|
*Fixes*
|
|||
|
|
⁃ Reduce potential length of macro-expanded docstrings to prevent
|
|||
|
|
byte-compiler warnings in downstream packages.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.2 0.10.1
|
|||
|
|
──────────
|
|||
|
|
|
|||
|
|
*Examples*
|
|||
|
|
⁃ Fixed byte-compilation of `diredy.el'.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.3 0.10
|
|||
|
|
────────
|
|||
|
|
|
|||
|
|
*Examples*
|
|||
|
|
⁃ Deffy
|
|||
|
|
⁃ Fix: Disambiguate forms with the same name.
|
|||
|
|
⁃ Diredy: Various small improvements.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.4 0.9
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
4.4.1 Changes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Library `taxy-magit-section' is now [packaged separately] and
|
|||
|
|
maintained in a [separate branch].
|
|||
|
|
⁃ Minor fixes to documentation and examples.
|
|||
|
|
|
|||
|
|
|
|||
|
|
[packaged separately]
|
|||
|
|
<https://elpa.gnu.org/packages/taxy-magit-section.html>
|
|||
|
|
|
|||
|
|
[separate branch]
|
|||
|
|
<https://github.com/alphapapa/taxy.el/tree/package/taxy-magit-section>
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.5 0.8
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
4.5.1 Additions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Short documentation group for Emacs 28+.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.5.2 Fixes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Require `map' for `pcase' pattern.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.6 0.7
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
4.6.1 Additions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Function `taxy-flatten' returns a list of the items in a taxy and
|
|||
|
|
its sub-taxys.
|
|||
|
|
⁃ Function/macro reference documentation.
|
|||
|
|
⁃ Example application `bookmarky' lists Emacs bookmarks grouped with
|
|||
|
|
Taxy.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.7 0.6
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
4.7.1 Additions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Sorting functions:
|
|||
|
|
⁃ `taxy-sort-items' (alias: `taxy-sort') sorts the items in a taxy
|
|||
|
|
and its sub-taxys.
|
|||
|
|
⁃ `taxy-sort-taxys' (alias: `taxy-sort*') sorts a taxy's sub-taxys.
|
|||
|
|
⁃ Defining classification domain-specific languages:
|
|||
|
|
⁃ Macro `taxy-define-key-definer' defines a key-function-defining
|
|||
|
|
macro.
|
|||
|
|
⁃ Function `taxy-make-take-function' makes a `:take' function using
|
|||
|
|
a list of key functions and a set of classification keys.
|
|||
|
|
⁃ Table-like, column-based formatting system for `taxy-magit-section':
|
|||
|
|
⁃ Function `taxy-magit-section-format-items', which formats items by
|
|||
|
|
columns.
|
|||
|
|
⁃ Variable `taxy-magit-section-insert-indent-items', which controls
|
|||
|
|
whether `taxy-magit-section-insert' applies indentation to each
|
|||
|
|
item. (Used to disable that behavior when items are pre-indented
|
|||
|
|
strings, e.g. as formatted by `taxy-magit-section-format-items'.)
|
|||
|
|
⁃ Example application `deffy', which shows an overview of top-level
|
|||
|
|
definitions and forms in an Elisp project or file. (Likely to be
|
|||
|
|
published as a separate package later.)
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.8 0.5
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
4.8.1 Additions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Function `taxy-magit-section-insert' takes new arguments:
|
|||
|
|
• `:initial-depth' sets the level at which the first level of
|
|||
|
|
hierarchy is considered to be at, for purposes of indentation.
|
|||
|
|
Setting it to a negative number prevents indentation of so many
|
|||
|
|
levels (i.e. setting it to -1 causes the first two levels to be
|
|||
|
|
unindented, since the first level will be considered to be at
|
|||
|
|
depth -1, and the second at depth 0).
|
|||
|
|
• `:blank-between-depth' sets the level up to which blank lines
|
|||
|
|
are inserted between sections (i.e. setting it to 1 causes blank
|
|||
|
|
lines to be inserted between sections up to depth 1, but not
|
|||
|
|
between sections deeper than that).
|
|||
|
|
⁃ Struct `taxy-magit-section' has a new `heading-face' slot, a
|
|||
|
|
function which takes a depth level argument and returns the face
|
|||
|
|
with which to propertize that section's heading.
|
|||
|
|
⁃ New example `taxy-package-report'.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.8.2 Fixes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Example `diredy' referred to an old function name.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.9 0.4
|
|||
|
|
───────
|
|||
|
|
|
|||
|
|
⁃ Incremented version to cause a new ELPA release (since removing a
|
|||
|
|
file that wasn't intended to be distributed on ELPA).
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.10 0.3
|
|||
|
|
────────
|
|||
|
|
|
|||
|
|
4.10.1 Changes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Within the `taxy' struct and related functions, the term `objects'
|
|||
|
|
is renamed to `items', which is shorter and has the same meaning.
|
|||
|
|
This makes code a bit more concise (e.g. `(taxy-objects taxy)'
|
|||
|
|
becomes `(taxy-items taxy)').
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.10.2 Fixes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Function `taxy-fill' always calls a taxy's `:take' function if
|
|||
|
|
defined. (Fixing "chains" of dynamic taxys.)
|
|||
|
|
⁃ Function `taxy-magit-section-insert' applies text properties from
|
|||
|
|
the inserted string to the indentation string (so commands that
|
|||
|
|
rely on text properties at the beginning of a line will work).
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.11 0.2
|
|||
|
|
────────
|
|||
|
|
|
|||
|
|
4.11.1 Changes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Function `taxy-take-keyed*' is renamed to `taxy-take-keyed',
|
|||
|
|
replacing the old function: it's more powerful, and there's little
|
|||
|
|
reason to maintain two versions.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.11.2 Additions
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ Struct `taxy' now has a `:make' slot, a function called to make new
|
|||
|
|
sub-taxys by `take-take-keyed' (defaulting to `make-taxy'). This
|
|||
|
|
is useful when defining structs specialized on `taxy'.
|
|||
|
|
⁃ Struct `taxy-magit-section' now has an `:indent' slot, a number of
|
|||
|
|
characters by which to indent each level of sub-taxy, applied
|
|||
|
|
automatically by function `taxy-magit-section-insert'.
|
|||
|
|
⁃ Each element of the new `taxy-take-keyed''s `KEY-FNS' argument may
|
|||
|
|
now be a function or a list of functions (or a list of a list of
|
|||
|
|
functions, etc.). Lists of functions create "chains" of
|
|||
|
|
independent, dynamic taxys descending from a single root taxy. See
|
|||
|
|
.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.11.3 Fixes
|
|||
|
|
╌╌╌╌╌╌╌╌╌╌╌╌
|
|||
|
|
|
|||
|
|
⁃ `taxy-magit-section''s `insert-object' function.
|
|||
|
|
⁃ `taxy-fill' now applies objects to the root taxy if no sub-taxys
|
|||
|
|
take them.
|
|||
|
|
|
|||
|
|
|
|||
|
|
4.12 0.1
|
|||
|
|
────────
|
|||
|
|
|
|||
|
|
First tagged version.
|
|||
|
|
|
|||
|
|
|
|||
|
|
5 Development
|
|||
|
|
═════════════
|
|||
|
|
|
|||
|
|
Bug reports, feature requests, suggestions — /oh my/!
|
|||
|
|
|
|||
|
|
|
|||
|
|
5.1 Copyright assignment
|
|||
|
|
────────────────────────
|
|||
|
|
|
|||
|
|
This package is part of [GNU Emacs], being distributed in [GNU ELPA].
|
|||
|
|
Contributions to this project must follow GNU guidelines, which means
|
|||
|
|
that, as with other parts of Emacs, patches of more than a few lines
|
|||
|
|
must be accompanied by having assigned copyright for the contribution
|
|||
|
|
to the FSF. Contributors who wish to do so may contact
|
|||
|
|
[emacs-devel@gnu.org] to request the assignment form.
|
|||
|
|
|
|||
|
|
|
|||
|
|
[GNU Emacs] <https://www.gnu.org/software/emacs/>
|
|||
|
|
|
|||
|
|
[GNU ELPA] <https://elpa.gnu.org/>
|
|||
|
|
|
|||
|
|
[emacs-devel@gnu.org] <mailto:emacs-devel@gnu.org>
|
|||
|
|
|
|||
|
|
|
|||
|
|
6 Credits
|
|||
|
|
═════════
|
|||
|
|
|
|||
|
|
⁃ Thanks to Stefan Monnier for his feedback, and for maintaining GNU
|
|||
|
|
ELPA.
|
|||
|
|
|
|||
|
|
|
|||
|
|
7 License
|
|||
|
|
═════════
|
|||
|
|
|
|||
|
|
GPLv3
|