about summary refs log blame commit diff
path: root/emacs/.emacs.d/wpc/alist.el
blob: 52493c6c03a744f52cb3dbbc16aee3242aa9b299 (plain) (tree)
1
2
3
4
5
6
                                                                                        
 
                                              


                                                 




















































































                                                                                



                                                                                
                               





                                                                                
                                                                       
                   




                              
                         
                     
                           


                                 
                               
 
                          






                                                                             
                       



                                                                               
                             





                                                                         
                            
                                          
                                                                          
                                                                  
                                   
                                                               
                                                   
 
                             
                                  

                                                

                      
                              
                                                                   


                           


                                                                                
                          

                                                                          
                                                                 

                           
                           
                                 
                                    



                                 
                             

                                                         
                                


                                                                  
                      


                                    
                        


                                      
                            
                                          
                             
 
                              
                                    
                              
 
                       


                                       

                                                                           



                                                                              
                                                                         



                    
                            
                                                      
                       


                                                 
                              
                                                      
                       


                                                 
                              
                                                          
                                                             

                       
                                   
                                                            
 
                        

                                                      
                                 






                                      
               




                                            











                                                         
 
                                                                                
        

                                                                                
                         
                 

                      
                              

                                         
                 

                      
                                


                                            




                                               
;;; alist.el --- Interface for working with associative lists -*- lexical-binding: t -*-

;; Author: William Carroll <wpcarro@gmail.com>
;; Version: 0.0.1
;; URL: https://git.wpcarro.dev/wpcarro/briefcase
;; Package-Requires: ((emacs "25.1"))

;;; 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.
;;
;; Similar libraries:
;; - map.el: Comes bundled with recent versions of Emacs.
;; - asoc.el: Helpers for working with alists.  asoc.el is similar to alist.el
;;   because it uses the "!" convention for signalling that a function mutates
;;   the underlying data structure.
;; - ht.el: Hash table library.
;; - kv.el: Library for dealing with key-value collections.  Note that map.el
;;   has a similar typeclass because it works with lists, hash-tables, or
;;   arrays.
;; - a.el: Clojure-inspired way of working with key-value data structures in
;; Elisp.  Works with alists, hash-tables, and sometimes vectors.
;;
;; 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"))
;;
;; Special thanks to github.com/alphapapa/emacs-package-dev-handbook for some of
;; the idiomatic ways to update alists.
;;
;; TODO: Include a section that compares alist.el to a.el from
;; github.com/plexus/a.el.

;; 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 'maybe)
(require 'macros)
(require 'dash)
(require 'tuple)
(require 'maybe)

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

;; TODO: Consider wrapping all of this with `(cl-defstruct alist xs)'.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defconst alist-enable-tests? t
  "When t, run the test suite.")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Library
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; TODO: Support a variadic version of this to easily construct alists.
(defun alist-new ()
  "Return a new, empty alist."
  '())

;; Create
;; TODO: See if this mutates.
(defun alist-set (k v xs)
  "Set K to V in XS."
  (if (alist-has-key? k xs)
      (progn
        (setf (alist-get k xs) v)
        xs)
    (list-cons `(,k . ,v) xs)))

(defun alist-set! (k v xs)
  "Set K to V in XS mutatively.
Note that this doesn't append to the alist in the way that most alists handle
  writing.  If the k already exists in XS, it is overwritten."
  (map-delete xs k)
  (map-put xs k v))

;; 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.
If `K' is not in `XS', this function errors.  Use `alist-upsert' if you're
interested in inserting a value when a key doesn't already exist."
  (if (maybe-nil? (alist-get k xs))
      (error "Refusing to update: key does not exist in alist")
    (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))

;; TODO: Support this.
(defun alist-upsert (k v f xs)
  "If K exists in `XS' call `F' on the value otherwise insert `V'."
  (if (alist-get k xs)
      (alist-update k f xs)
    (alist-set k v 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."
  (maybe-some? (assoc k xs)))

(defun alist-has-value? (v xs)
  "Return t if XS has a value of V."
  (maybe-some? (rassoc v xs)))

(defun alist-count (xs)
  "Return the number of entries in XS."
  (length xs))

;; TODO: Should I support `alist-find-key' and `alist-find-value' variants?
(defun alist-find (p xs)
  "Find an element in XS.

Apply a predicate fn, P, to each key and value in XS and return the key of the
first element that returns t."
  (let ((result (list-find (lambda (x) (funcall p (car x) (cdr x))) xs)))
    (if result
        (car result)
      nil)))

(defun alist-map-keys (f xs)
  "Call F on the values in XS, returning a new alist."
  (list-map (lambda (x)
              `(,(funcall f (car x)) . ,(cdr x)))
            xs))

(defun alist-map-values (f xs)
  "Call F on the values in XS, returning a new alist."
  (list-map (lambda (x)
              `(,(car x) . ,(funcall f (cdr x))))
            xs))

(defun alist-reduce (acc f xs)
  "Return a new alist by calling F on k v and ACC from XS.
F should return a tuple.  See tuple.el for more information."
  (->> (alist-keys xs)
       (list-reduce acc
                    (lambda (k acc)
                      (funcall f k (alist-get k xs) acc)))))

(defun alist-merge (a b)
  "Return a new alist with a merge of alists, A and B.
In this case, the last writer wins, which is B."
  (alist-reduce a #'alist-set b))

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

(when alist-enable-tests?
  (prelude-assert
   (equal '((2 . one)
            (3 . two))
          (alist-map-keys #'1+
                          '((1 . one)
                            (2 . two)))))
  (prelude-assert
   (equal '((one . 2)
            (two . 3))
          (alist-map-values #'1+
                            '((one . 1)
                              (two . 2))))))


;; TODO: Support test cases for the entire API.

(provide 'alist)
;;; alist.el ends here