Created
September 30, 2019 13:47
-
-
Save wpcarro/7786985c6150ace608f2fe31ea7d5970 to your computer and use it in GitHub Desktop.
A rough draft of a module to improve the ergonomics of working with associative lists in Emacs Lisp.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; 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. | |
| ;; | |
| ;; 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")) | |
| ;; 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 'prelude) | |
| (require 'macros) | |
| (require 'dash) | |
| ;;; 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. | |
| ;; Create | |
| (defun alist/set (k v xs) | |
| "Set K to V in XS. | |
| This will replace the first occurrence of K with V. This is different from the | |
| typical idiomatic Elispy way of setting values where `add-to-list' is preferred | |
| to shadow existing values. If this value behavior is desired see | |
| `alist/shadow'." | |
| ;; TODO: Replace `-map-when' with a better `reduce-until' function that aborts | |
| ;; after a single replacement. | |
| (let ((map? t)) | |
| (-map-when (lambda (_) map?) | |
| (lambda (entry) | |
| (if (equal k (car entry)) | |
| (progn | |
| (setq map? nil) | |
| `(,k . ,v)) | |
| entry)) | |
| xs))) | |
| (defun alist/set! (k v xs) | |
| "Set K to V in XS mutatively." | |
| (setf (alist-get k xs) v) | |
| xs) | |
| ;; 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." | |
| (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)) | |
| ;; 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." | |
| (prelude/set? (assoc k xs))) | |
| (defun alist/has-value? (v xs) | |
| "Return t if XS has a value of V." | |
| (prelude/set? (rassoc v xs))) | |
| (defun alist/count (xs) | |
| "Return the number of entries in XS." | |
| (length xs)) | |
| ;; 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 | |
| ;; TODO: Support test cases for the entire API. | |
| (provide 'alist) | |
| ;;; alist.el ends here |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment