· 6 years ago · Oct 18, 2019, 10:38 PM
1;;; alist.el --- Interface for working with associative lists -*- lexical-binding: t -*-
2;; Author: William Carroll <wpcarro@gmail.com>
3
4;;; Commentary:
5;; Firstly, a rant:
6;; In most cases, I find Elisp's APIs to be confusing. There's a mixture of
7;; overloaded functions that leak the implementation details (TODO: provide an
8;; example of this.) of the abstract data type, which I find privileges those
9;; "insiders" who spend disproportionately large amounts of time in Elisp land,
10;; and other functions with little-to-no pattern about the order in which
11;; arguments should be applied. In theory, however, most of these APIs could
12;; and should be much simpler. This module represents a step in that direction.
13;;
14;; I'm modelling these APIs after Elixir's APIs.
15;;
16;; On my wishlist is to create protocols that will allow generic interfaces like
17;; Enum protocols, etc. Would be nice to abstract over...
18;; - associative lists (i.e. alists)
19;; - property lists (i.e. plists)
20;; - hash tables
21;; ...with some dictionary or map-like interface. This will probably end up
22;; being quite similar to the kv.el project but with differences at the API
23;; layer.
24;;
25;; Some API design principles:
26;; - The "noun" (i.e. alist) of the "verb" (i.e. function) comes last to improve
27;; composability with the threading macro (i.e. `->>') and to improve consumers'
28;; intuition with the APIs. Learn this once, know it always.
29;;
30;; - Every function avoids mutating the alist unless it ends with !.
31;;
32;; - CRUD operations will be named according to the following table:
33;; - "create" *and* "set"
34;; - "read" *and* "get"
35;; - "update"
36;; - "delete" *and* "remove"
37;;
38;; For better or worse, all of this code expects alists in the form of:
39;; ((first-name . "William") (last-name . "Carroll"))
40
41;; Dependencies:
42
43;; TODO: Consider dropping explicit dependency white-listing since all of these
44;; should be available in my Emacs. The problem arises when this library needs
45;; to be published, in which case, something like Nix and a build process could
46;; possible insert the necessary require statements herein. Not sure how I feel
47;; about this though.
48(require 'prelude)
49(require 'macros)
50(require 'dash)
51
52;;; Code:
53
54;; TODO: Support function aliases for:
55;; - create/set
56;; - read/get
57;; - update
58;; - delete/remove
59
60;; Support mutative variants of functions with an ! appendage to their name.
61
62;; Ensure that the same message about only updating the first occurrence of a
63;; key is consistent throughout documentation using string interpolation or some
64;; other mechanism.
65
66;; Create
67(defun alist/set (k v xs)
68 "Set K to V in XS.
69This will replace the first occurrence of K with V. This is different from the
70typical idiomatic Elispy way of setting values where `add-to-list' is preferred
71to shadow existing values. If this value behavior is desired see
72`alist/shadow'."
73 ;; TODO: Replace `-map-when' with a better `reduce-until' function that aborts
74 ;; after a single replacement.
75 (let ((map? t))
76 (-map-when (lambda (_) map?)
77 (lambda (entry)
78 (if (equal k (car entry))
79 (progn
80 (setq map? nil)
81 `(,k . ,v))
82 entry))
83 xs)))
84
85(defun alist/set! (k v xs)
86 "Set K to V in XS mutatively."
87 (setf (alist-get k xs) v)
88 xs)
89
90;; Read
91(defun alist/get (k xs)
92 "Return the value at K in XS; otherwise, return nil.
93Returns the first occurrence of K in XS since alists support multiple entries."
94 (cdr (assoc k xs)))
95
96(defun alist/get-entry (k xs)
97 "Return the first key-value pair at K in XS."
98 (assoc k xs))
99
100;; Update
101;; TODO: Add warning about only the first occurrence being updated in the
102;; documentation.
103(defun alist/update (k f xs)
104 "Apply F to the value stored at K in XS."
105 (alist/set k (funcall f (alist/get k xs)) xs))
106
107(defun alist/update! (k f xs)
108 "Call F on the entry at K in XS.
109Mutative variant of `alist/update'."
110 (alist/set! k (funcall f (alist/get k xs))xs))
111
112;; Delete
113;; TODO: Make sure `delete' and `remove' behave as advertised in the Elisp docs.
114(defun alist/delete (k xs)
115 "Deletes the entry of K from XS.
116This only removes the first occurrence of K, since alists support multiple
117 key-value entries. See `alist/delete-all' and `alist/dedupe'."
118 (remove (assoc k xs) xs))
119
120(defun alist/delete! (k xs)
121 "Delete the entry of K from XS.
122Mutative variant of `alist/delete'."
123 (delete (assoc k xs) xs))
124
125;; Additions to the CRUD API
126;; TODO: Implement this function.
127(defun alist/dedupe-keys (xs)
128 "Remove the entries in XS where the keys are `equal'.")
129
130(defun alist/dedupe-entries (xs)
131 "Remove the entries in XS where the key-value pair are `equal'."
132 (delete-dups xs))
133
134(defun alist/keys (xs)
135 "Return a list of the keys in XS."
136 (mapcar 'car xs))
137
138(defun alist/values (xs)
139 "Return a list of the values in XS."
140 (mapcar 'cdr xs))
141
142(defun alist/has-key? (k xs)
143 "Return t if XS has a key `equal' to K."
144 (prelude/set? (assoc k xs)))
145
146(defun alist/has-value? (v xs)
147 "Return t if XS has a value of V."
148 (prelude/set? (rassoc v xs)))
149
150(defun alist/count (xs)
151 "Return the number of entries in XS."
152 (length xs))
153
154;; TODO: Support `-all' variants like:
155;; - get-all
156;; - delete-all
157;; - update-all
158
159;; Scratch-pad
160(macros/comment
161 (progn
162 (setq person '((first-name . "William")
163 (first-name . "William")
164 (last-name . "Carroll")
165 (last-name . "Another")))
166 (alist/set 'last-name "Van Gogh" person)
167 (alist/get 'last-name person)
168 (alist/update 'last-name (lambda (x) "whoops") person)
169 (alist/delete 'first-name person)
170 (alist/keys person)
171 (alist/values person)
172 (alist/count person)
173 (alist/has-key? 'first-name person)
174 (alist/has-value? "William" person)
175 ;; (alist/dedupe-keys person)
176 (alist/dedupe-entries person)
177 (alist/count person)))
178
179;; Tests
180
181;; TODO: Support test cases for the entire API.
182
183(provide 'alist)
184;;; alist.el ends here