-
Notifications
You must be signed in to change notification settings - Fork 0
/
a-20210929.1510.el
357 lines (304 loc) · 12.1 KB
/
a-20210929.1510.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
;;; a.el --- Associative data structure functions -*- lexical-binding: t; -*-
;; Copyright (C) 2017-2021 Arne Brasseur
;; Author: Arne Brasseur <[email protected]>
;; URL: https://github.com/plexus/a.el
;; Package-Version: 20210929.1510
;; Package-Commit: 9ad2d18252b729174fe22ed0b2b7670c88f60c31
;; Keywords: lisp
;; Version: 1.0.0
;; Package-Requires: ((emacs "25"))
;; This file is not part of GNU Emacs.
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;; Library for dealing with associative data structures: alists, hash-maps, and
;; vectors (for vectors, the indices are treated as keys).
;;
;; This library is largely inspired by Clojure, it has many of the functions
;; found in clojure.core, prefixed with `a-'. All functions treat their
;; arguments as immutable, so e.g. `a-assoc' will clone the hash-table or alist
;; it is given. Keep this in mind when writing performance sensitive code.
;;; Code:
(eval-when-compile (require 'subr-x)) ;; for things like hash-table-keys
(require 'cl-lib)
(require 'seq)
(defun a-associative-p (obj)
(or (not obj)
(hash-table-p obj)
(and (consp obj)
(consp (car obj)))))
(defalias 'a-associative? 'a-associative-p)
(defun a-get (map key &optional not-found)
"Return the value MAP mapped to KEY, NOT-FOUND or nil if key not present."
(cond
;; own implementation instead of alist-get so keys are checked with equal
;; instead of eq
((listp map)
(a--alist-get map key not-found))
((vectorp map)
(if (a-has-key? map key)
(aref map key)
not-found))
((hash-table-p map)
(gethash key map not-found))
(t (user-error "Not associative: %S" map))))
(defun a--alist-get (map key &optional not-found)
"Like alist-get, but uses equal instead of eq to look up in map MAP key KEY.
Returns NOT-FOUND if the key is not present, or `nil' if
NOT-FOUND is not specified."
(cl-block nil
(seq-doseq (pair map)
(when (equal (car pair) key)
(cl-return (cdr pair))))
not-found))
(defun a-get-in (m ks &optional not-found)
"Look up a value in a nested associative structure.
Given a data structure M, and a sequence of keys KS, find the
value found by using each key in turn to do a lookup in the next
\"layer\". Return `nil' if the key is not present, or the NOT-FOUND
value if supplied."
(let ((result m))
(cl-block nil
(seq-doseq (k ks)
(if (a-has-key? result k)
(setq result (a-get result k))
(cl-return not-found)))
result)))
(defmacro a-get* (&rest keys)
"Look up a value in a nested associative structure.
Like a-get-in, but takes the key sequence KEYS directly as vararg
arguments, rather than as a single sequence."
(cl-labels ((rec (keys)
`(a-get ,(if (and (consp (cdr keys))
(cddr keys))
(rec (cdr keys))
(cadr keys))
,(car keys))))
(rec (nreverse keys))))
(defun a-has-key (coll k)
"Check if the given associative collection COLL has a certain key K."
(cond
((listp coll) (not (eq (a--alist-get coll k :not-found) :not-found)))
((vectorp coll) (and (integerp k) (< -1 k (length coll))))
((hash-table-p coll) (not (eq (gethash k coll :not-found) :not-found)))
(t (user-error "Not associative: %S" coll))))
(defalias 'a-has-key? 'a-has-key)
(defun a-assoc-1 (coll k v)
"Like `a-assoc', (in COLL assoc K with V) but only takes a single k-v pair.
Internal helper function."
(cond
((listp coll)
(if (a-has-key? coll k)
(mapcar (lambda (entry)
(if (equal (car entry) k)
(cons k v)
entry))
coll)
(cons (cons k v) coll)))
((vectorp coll)
(if (and (integerp k) (>= k 0))
(if (< k (length coll))
(let ((copy (copy-sequence coll)))
(aset copy k v)
copy)
(vconcat coll (make-list (- k (length coll)) nil) (list v)))))
((hash-table-p coll)
(let ((copy (copy-hash-table coll)))
(puthash k v copy)
copy))))
(defun a-assoc (coll &rest kvs)
"Return an updated collection COLL, associating values with keys KVS."
(when (not (cl-evenp (a-count kvs)))
(user-error "a-assoc requires an even number of arguments!"))
(seq-reduce (lambda (coll kv)
(seq-let [k v] kv
(a-assoc-1 coll k v)))
(seq-partition kvs 2)
coll))
(defun a-keys (coll)
"Return the keys in the collection COLL."
(cond
((listp coll)
(mapcar #'car coll))
((hash-table-p coll)
(hash-table-keys coll))))
(defun a-vals (coll)
"Return the values in the collection COLL."
(cond
((listp coll)
(mapcar #'cdr coll))
((hash-table-p coll)
(hash-table-values coll))))
(defun a-reduce-kv (fn from coll)
"Reduce with FN starting from FROM the collection COLL.
Reduce an associative collection COLL, starting with an initial
value of FROM. The reducing function FN receives the intermediate
value, key, and value."
(seq-reduce (lambda (acc key)
(funcall fn acc key (a-get coll key)))
(a-keys coll)
from))
(defun a-count (coll)
"Count the number of key-value pairs in COLL.
Like length, but can also return the length of hash tables."
(cond
((seqp coll)
(length coll))
((hash-table-p coll)
(hash-table-count coll))))
(defun a-equal (a b)
"Compare collections A, B for value equality.
Associative collections (hash tables and a-lists) are considered
equal if they contain equal key-value pairs, regardless of order.
Sequences (lists or vectors) are considered equal if they contain
the same elements in the same order.
Collection elements are compared using `a-equal'. In other words,
the equality check is recursive, resulting in a \"deep\" equality
check.
Anything that isn't associative or a sequence is compared with
`equal'."
(cond
((and (a-associative? a) (a-associative? b))
(or (equal a b)
(when (eq (a-count a) (a-count b))
(cl-block nil
(seq-doseq (k (a-keys a))
(when (not (and
(a-has-key b k)
(a-equal (a-get a k) (a-get b k))))
(cl-return nil)))
(seq-doseq (k (a-keys b))
(when (not (and
(a-has-key a k)
(a-equal (a-get a k) (a-get b k))))
(cl-return nil)))
t))))
((and (sequencep a) (sequencep b))
(and (eq (length a) (length b))
(or (and (seq-empty-p a) (seq-empty-p b))
(and (a-equal (elt a 0) (elt b 0))
(a-equal (seq-drop a 1) (seq-drop b 1))))))
(t
(equal a b))))
(defalias 'a-equal? 'a-equal)
(defun a-merge (&rest colls)
"Merge multiple associative collections.
Return the type of the first collection COLLS."
(seq-reduce (lambda (this that)
(a-reduce-kv (lambda (coll k v)
(a-assoc coll k v))
this
that))
(cdr colls)
(car colls)))
(defun a-merge-with (f &rest colls)
"Merge multiple associative collections.
Return the type of the first collection COLLS. If a key exists in
both, then combine the associated values by calling f on them."
(seq-reduce (lambda (this that)
(a-reduce-kv (lambda (coll k v)
(a-assoc coll k (if (a-has-key coll k)
(funcall f v (a-get coll k))
v)))
this
that))
(cdr colls)
(car colls)))
(defun a-alist (&rest kvs)
"Create an association list from the given keys and values KVS.
Arguments are simply provided in sequence, rather than as lists or cons cells.
For example: (a-alist :foo 123 :bar 456)"
(mapcar (lambda (kv) (cons (car kv) (cadr kv))) (seq-partition kvs 2)))
(defalias 'a-list 'a-alist)
(defun a-hash-table (&rest kvs)
"Create a hash table from the given keys and values KVS.
Arguments are simply provided in sequence, rather than as lists
or cons cells. As \"test\" for the hash table, equal is used. The
hash table is created without extra storage space, so with a size
equal to amount of key-value pairs, since it is assumed to be
treated as immutable.
For example: (a-hash-table :foo 123 :bar 456)"
(let* ((kv-pairs (seq-partition kvs 2))
(hash-map (make-hash-table :test 'equal :size (length kv-pairs))))
(seq-do (lambda (pair)
(puthash (car pair) (cadr pair) hash-map))
kv-pairs)
hash-map))
(defun a-assoc-in (coll keys value)
"In collection COLL, at location KEYS, associate value VALUE.
Associates a value in a nested associative collection COLL, where
KEYS is a sequence of keys and VALUE is the new value and returns
a new nested structure. If any levels do not exist, association
lists will be created."
(cl-case (length keys)
(0 coll)
(1 (a-assoc-1 coll (elt keys 0) value))
(t (a-assoc-1 coll
(elt keys 0)
(a-assoc-in (a-get coll (elt keys 0))
(seq-drop keys 1)
value)))))
(defun a-dissoc--list (list keys)
"Return updated LIST with KEYS removed.
Internal helper. Use `a-dissoc' instead."
(a-reduce-kv (lambda (res k v)
(if (member k keys)
res
(cons (cons k v) res)))
nil
list))
(defun a-dissoc--hash-table (table keys)
"Return updated TABLE with KEYS removed.
Internal helper. Use `a-dissoc' instead."
(let ((new-table (make-hash-table :size (hash-table-count table)
:test (hash-table-test table)))
(rest-keys (seq-remove (lambda (k)
(member k keys))
(a-keys table))))
(seq-doseq (k rest-keys)
(puthash k (gethash k table) new-table))
new-table))
(defun a-dissoc (coll &rest keys)
"Return an updated version of collection COLL with the KEY removed."
(cond
((listp coll) (a-dissoc--list coll keys))
((hash-table-p coll) (a-dissoc--hash-table coll keys))))
(defun a-update (coll key fn &rest args)
"In collection COLL, at location KEY, apply FN with extra args ARGS.
'Updates' a value in an associative collection COLL, where KEY is
a key and FN is a function that will take the old value and any
supplied args and return the new value, and returns a new
structure. If the key does not exist, nil is passed as the old
value."
(a-assoc-1 coll
key
(apply #'funcall fn (a-get coll key) args)))
(defun a-update-in (coll keys fn &rest args)
"In collection COLL, at location KEYS, apply FN with extra args ARGS.
'Updates' a value in a nested associative collection COLL, where
KEYS is a sequence of keys and FN is a function that will take
the old value and any supplied ARGS and return the new value, and
returns a new nested structure. If any levels do not exist,
association lists will be created."
(cl-case (length keys)
(0 coll)
(1 (apply #'a-update coll (elt keys 0) fn args))
(t (a-assoc-1 coll
(elt keys 0)
(apply #'a-update-in
(a-get coll (elt keys 0))
(seq-drop keys 1)
fn
args)))))
(provide 'a)
;;; a.el ends here