|
|
- ;;; a.el --- Associative data structure functions -*- lexical-binding: t; -*-
-
- ;; Copyright (C) 2017 Arne Brasseur
-
- ;; Author: Arne Brasseur <arne@arnebrasseur.net>
- ;; URL: https://github.com/plexus/a.el
- ;; Package-Version: 20180907.953
- ;; Keywords: lisp
- ;; Version: 0.1.1
- ;; 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 (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
|