diff options
Diffstat (limited to 'emacs/.emacs.d/wpc/alist.el')
-rw-r--r-- | emacs/.emacs.d/wpc/alist.el | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/emacs/.emacs.d/wpc/alist.el b/emacs/.emacs.d/wpc/alist.el new file mode 100644 index 000000000000..f23109ce6a38 --- /dev/null +++ b/emacs/.emacs.d/wpc/alist.el @@ -0,0 +1,277 @@ +;;; alist.el --- Interface for working with associative lists -*- lexical-binding: t -*- +;; Author: William Carroll <wpcarro@gmail.com> + +;;; Commentary: +;; Firstly, a rant: +;; In most cases, I find Elisp's APIs to be confusing. There's a mixture of +;; overloaded functions that leak the implementation details (TODO: provide an +;; example of this.) of the abstract data type, which I find privileges those +;; "insiders" who spend disproportionately large amounts of time in Elisp land, +;; and other functions with little-to-no pattern about the order in which +;; arguments should be applied. In theory, however, most of these APIs could +;; and should be much simpler. This module represents a step in that direction. +;; +;; I'm modelling these APIs after Elixir's APIs. +;; +;; On my wishlist is to create protocols that will allow generic interfaces like +;; Enum protocols, etc. Would be nice to abstract over... +;; - associative lists (i.e. alists) +;; - property lists (i.e. plists) +;; - hash tables +;; ...with some dictionary or map-like interface. This will probably end up +;; being quite similar to the kv.el project but with differences at the API +;; layer. +;; +;; Similar libraries: +;; - map.el: Comes bundled with recent versions of Emacs. +;; - asoc.el: Helpers for working with alists. asoc.el is similar to alist.el +;; because it uses the "!" convention for signalling that a function mutates +;; the underlying data structure. +;; - ht.el: Hash table library. +;; - kv.el: Library for dealing with key-value collections. Note that map.el +;; has a similar typeclass because it works with lists, hash-tables, or +;; arrays. +;; - a.el: Clojure-inspired way of working with key-value data structures in +;; Elisp. Works with alists, hash-tables, and sometimes vectors. +;; +;; Some API design principles: +;; - The "noun" (i.e. alist) of the "verb" (i.e. function) comes last to improve +;; composability with the threading macro (i.e. `->>') and to improve consumers' +;; intuition with the APIs. Learn this once, know it always. +;; +;; - Every function avoids mutating the alist unless it ends with !. +;; +;; - CRUD operations will be named according to the following table: +;; - "create" *and* "set" +;; - "read" *and* "get" +;; - "update" +;; - "delete" *and* "remove" +;; +;; For better or worse, all of this code expects alists in the form of: +;; ((first-name . "William") (last-name . "Carroll")) +;; +;; Special thanks to github.com/alphapapa/emacs-package-dev-handbook for some of +;; the idiomatic ways to update alists. +;; +;; TODO: Include a section that compares alist.el to a.el from +;; github.com/plexus/a.el. + +;; Dependencies: + +;; TODO: Consider dropping explicit dependency white-listing since all of these +;; should be available in my Emacs. The problem arises when this library needs +;; to be published, in which case, something like Nix and a build process could +;; possible insert the necessary require statements herein. Not sure how I feel +;; about this though. +(require 'maybe) +(require 'macros) +(require 'dash) +(require 'tuple) +(require 'maybe) + +;;; Code: + +;; TODO: Support function aliases for: +;; - create/set +;; - read/get +;; - update +;; - delete/remove + +;; Support mutative variants of functions with an ! appendage to their name. + +;; Ensure that the same message about only updating the first occurrence of a +;; key is consistent throughout documentation using string interpolation or some +;; other mechanism. + +;; TODO: Consider wrapping all of this with `(cl-defstruct alist xs)'. + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Constants +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defconst alist/enable-tests? t + "When t, run the test suite.") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Library +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; TODO: Support a variadic version of this to easily construct alists. +(defun alist/new () + "Return a new, empty alist." + '()) + +;; Create +;; TODO: See if this mutates. +(defun alist/set (k v xs) + "Set K to V in XS." + (if (alist/has-key? k xs) + (progn + (setf (alist-get k xs) v) + xs) + (list/cons `(,k . ,v) xs))) + +(defun alist/set! (k v xs) + "Set K to V in XS mutatively. +Note that this doesn't append to the alist in the way that most alists handle + writing. If the k already exists in XS, it is overwritten." + (map-delete xs k) + (map-put xs k v)) + +;; Read +(defun alist/get (k xs) + "Return the value at K in XS; otherwise, return nil. +Returns the first occurrence of K in XS since alists support multiple entries." + (cdr (assoc k xs))) + +(defun alist/get-entry (k xs) + "Return the first key-value pair at K in XS." + (assoc k xs)) + +;; Update +;; TODO: Add warning about only the first occurrence being updated in the +;; documentation. +(defun alist/update (k f xs) + "Apply F to the value stored at K in XS. +If `K' is not in `XS', this function errors. Use `alist/upsert' if you're +interested in inserting a value when a key doesn't already exist." + (if (maybe/nil? (alist/get k xs)) + (error "Refusing to update: key does not exist in alist") + (alist/set k (funcall f (alist/get k xs)) xs))) + +(defun alist/update! (k f xs) + "Call F on the entry at K in XS. +Mutative variant of `alist/update'." + (alist/set! k (funcall f (alist/get k xs))xs)) + +;; TODO: Support this. +(defun alist/upsert (k v f xs) + "If K exists in `XS' call `F' on the value otherwise insert `V'." + (if (alist/get k xs) + (alist/update k f xs) + (alist/set k v xs))) + +;; Delete +;; TODO: Make sure `delete' and `remove' behave as advertised in the Elisp docs. +(defun alist/delete (k xs) + "Deletes the entry of K from XS. +This only removes the first occurrence of K, since alists support multiple + key-value entries. See `alist/delete-all' and `alist/dedupe'." + (remove (assoc k xs) xs)) + +(defun alist/delete! (k xs) + "Delete the entry of K from XS. +Mutative variant of `alist/delete'." + (delete (assoc k xs) xs)) + +;; Additions to the CRUD API +;; TODO: Implement this function. +(defun alist/dedupe-keys (xs) + "Remove the entries in XS where the keys are `equal'.") + +(defun alist/dedupe-entries (xs) + "Remove the entries in XS where the key-value pair are `equal'." + (delete-dups xs)) + +(defun alist/keys (xs) + "Return a list of the keys in XS." + (mapcar 'car xs)) + +(defun alist/values (xs) + "Return a list of the values in XS." + (mapcar 'cdr xs)) + +(defun alist/has-key? (k xs) + "Return t if XS has a key `equal' to K." + (maybe/some? (assoc k xs))) + +(defun alist/has-value? (v xs) + "Return t if XS has a value of V." + (maybe/some? (rassoc v xs))) + +(defun alist/count (xs) + "Return the number of entries in XS." + (length xs)) + +;; TODO: Should I support `alist/find-key' and `alist/find-value' variants? +(defun alist/find (p xs) + "Apply a predicate fn, P, to each key and value in XS and return the key of + the first element that returns t." + (let ((result (list/find (lambda (x) (funcall p (car x) (cdr x))) xs))) + (if result + (car result) + nil))) + +(defun alist/map-keys (f xs) + "Call F on the values in XS, returning a new alist." + (list/map (lambda (x) + `(,(funcall f (car x)) . ,(cdr x))) + xs)) + +(defun alist/map-values (f xs) + "Call F on the values in XS, returning a new alist." + (list/map (lambda (x) + `(,(car x) . ,(funcall f (cdr x)))) + xs)) + +(defun alist/reduce (acc f xs) + "Return a new alist by calling F on k v and ACC from XS. +F should return a tuple. See tuple.el for more information." + (->> (alist/keys xs) + (list/reduce acc + (lambda (k acc) + (funcall f k (alist/get k xs) acc))))) + +(defun alist/merge (a b) + "Return a new alist with a merge of alists, A and B. +In this case, the last writer wins, which is B." + (alist/reduce a #'alist/set b)) + +;; TODO: Support `-all' variants like: +;; - get-all +;; - delete-all +;; - update-all + +;; Scratch-pad +(macros/comment + (progn + (setq person '((first-name . "William") + (first-name . "William") + (last-name . "Carroll") + (last-name . "Another"))) + (alist/set 'last-name "Van Gogh" person) + (alist/get 'last-name person) + (alist/update 'last-name (lambda (x) "whoops") person) + (alist/delete 'first-name person) + (alist/keys person) + (alist/values person) + (alist/count person) + (alist/has-key? 'first-name person) + (alist/has-value? "William" person) + ;; (alist/dedupe-keys person) + (alist/dedupe-entries person) + (alist/count person))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(when alist/enable-tests? + (prelude/assert + (equal '((2 . one) + (3 . two)) + (alist/map-keys #'1+ + '((1 . one) + (2 . two))))) + (prelude/assert + (equal '((one . 2) + (two . 3)) + (alist/map-values #'1+ + '((one . 1) + (two . 2)))))) + + +;; TODO: Support test cases for the entire API. + +(provide 'alist) +;;; alist.el ends here |