Skip to content

Instantly share code, notes, and snippets.

@wpcarro
Created September 30, 2019 13:47
Show Gist options
  • Select an option

  • Save wpcarro/7786985c6150ace608f2fe31ea7d5970 to your computer and use it in GitHub Desktop.

Select an option

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.
;;; 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