feat(wpcarro/emacs): Package al, list, set, struct

Originally I set-out to package `al.el`, but as I started traversing the
dependencies, I needed to package increasingly more packages. I refactored some
of these to prune their dependencies to slay this hydra before it turned into a
never-ending project. I have mixed feelings about this.

I also introduced `ert` and unit tests into my Elisp packaging, so it'll be nice
to have build-time tests that run when Emacs updates land in depot.

Change-Id: I2756dc60888b80255a495e08ae61bd547e6b3db2
Reviewed-on: https://cl.tvl.fyi/c/depot/+/5998
Reviewed-by: wpcarro <wpcarro@gmail.com>
Autosubmit: wpcarro <wpcarro@gmail.com>
Tested-by: BuildkiteCI
This commit is contained in:
William Carroll 2022-07-28 20:29:07 -07:00 committed by clbot
parent caf068253a
commit 15c9ff4902
13 changed files with 323 additions and 165 deletions

View file

@ -0,0 +1,28 @@
{ pkgs, depot, ... }:
let
list = pkgs.callPackage
({ emacsPackages }:
emacsPackages.trivialBuild {
pname = "list";
version = "1.0.0";
src = ./list.el;
packageRequires =
(with emacsPackages; [
dash
]) ++
(with depot.users.wpcarro.emacs.pkgs; [
set
]);
})
{ };
emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ list ]);
in
list.overrideAttrs (_old: {
doCheck = true;
checkPhase = ''
${emacs}/bin/emacs -batch \
-l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
'';
})

View file

@ -0,0 +1,193 @@
;;; list.el --- Functions for working with lists -*- lexical-binding: t -*-
;; Author: William Carroll <wpcarro@gmail.com>
;; Version: 0.0.1
;; Package-Requires: ((emacs "24"))
;;; Commentary:
;; Since I prefer having the `list-' namespace, I wrote this module to wrap many
;; of the functions that are defined in the the global namespace in ELisp. I
;; sometimes forget the names of these functions, so it's nice for them to be
;; organized like this.
;;
;; Motivation:
;; Here are some examples of function names that I cannot tolerate:
;; - `car': Return the first element (i.e. "head") of a linked list
;; - `cdr': Return the tail of a linked list
;; As are most APIs for standard libraries that I write, this is heavily
;; influenced by Elixir's standard library.
;;
;; Elixir's List library:
;; - ++/2
;; - --/2
;; - hd/1
;; - tl/1
;; - in/2
;; - length/1
;;
;; Similar libraries:
;; - dash.el: Functional library that mimmicks Clojure. It is consumed herein.
;; - list-utils.el: Utility library that covers things that dash.el may not
;; cover.
;; stream.el: Elisp implementation of streams, "implemented as delayed
;; evaluation of cons cells."
;; TODO: Consider naming this file linked-list.el.
;; TODO: Support module-like macro that auto-namespaces functions.
;; TODO: Consider wrapping most data structures like linked-lists,
;; associative-lists, etc in a `cl-defstruct', so that the dispatching by type
;; can be nominal instead of duck-typing. I'm not sure if this is a good idea
;; or not. If I do this, I should provide isomorphisms to map between idiomatic
;; ways of working with Elisp data structures and my wrapped variants.
;; TODO: Are function aliases/synonyms even a good idea? Or do they just
;; bloat the API unnecessarily?
;;; Code:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Dependencies
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require 'dash)
(require 'set)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Library
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun list-new ()
"Return a new, empty list."
'())
(defun list-concat (&rest lists)
"Joins `LISTS' into on list."
(apply #'-concat lists))
(defun list-join (joint xs)
"Join a list of strings, XS, with JOINT."
(if (list-empty? xs)
""
(list-reduce (list-first xs)
(lambda (x acc)
(string-concat acc joint x))
(list-tail xs))))
(defun list-length (xs)
"Return the number of elements in `XS'."
(length xs))
(defun list-get (i xs)
"Return the value in `XS' at `I', or nil."
(nth i xs))
(defun list-head (xs)
"Return the head of `XS'."
(car xs))
;; TODO: Learn how to write proper function aliases.
(defun list-first (xs)
"Alias for `list-head' for `XS'."
(list-head xs))
(defun list-tail (xs)
"Return the tail of `XS'."
(cdr xs))
(defun list-reverse (xs)
"Reverses `XS'."
(reverse xs))
(defun list-cons (x xs)
"Add `X' to the head of `XS'."
(cons x xs))
;; map, filter, reduce
;; TODO: Create function adapters like swap.
;; (defun adapter/swap (f)
;; "Return a new function that wraps `F' and swaps the arguments."
;; (lambda (a b)
;; (funcall f b a)))
;; TODO: Make this function work.
(defun list-reduce (acc f xs)
"Return over `XS' calling `F' on an element in `XS'and `ACC'."
(-reduce-from (lambda (acc x) (funcall f x acc)) acc xs))
(defun list-map (f xs)
"Call `F' on each element of `XS'."
(-map f xs))
(defun list-map-indexed (f xs)
"Call `F' on each element of `XS' along with its index."
(-map-indexed (lambda (i x) (funcall f x i)) xs))
(defun list-filter (p xs)
"Return a subset of XS where predicate P returned t."
(list-reverse
(list-reduce
'()
(lambda (x acc)
(if (funcall p x)
(list-cons x acc)
acc))
xs)))
(defun list-reject (p xs)
"Return a subset of XS where predicate of P return nil."
(list-filter (lambda (x) (not (funcall p x))) xs))
(defun list-find (p xs)
"Return the first x in XS that passes P or nil."
(-find p xs))
;; TODO: Support dedupe.
;; TODO: Should we call this unique? Or distinct?
;; TODO: Add tests.
(defun list-dedupe-adjacent (xs)
"Return XS without adjacent duplicates."
(list-reduce (list (list-first xs))
(lambda (x acc)
(if (equal x (list-first acc))
acc
(list-cons x acc)))
xs))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Predicates
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun list-instance? (xs)
"Return t if `XS' is a list.
Be leery of using this with things like alists. Many data structures in Elisp
are implemented using linked lists."
(listp xs))
(defun list-empty? (xs)
"Return t if XS are empty."
(= 0 (list-length xs)))
(defun list-all? (p xs)
"Return t if all `XS' pass the predicate, `P'."
(-all? p xs))
(defun list-any? (p xs)
"Return t if any `XS' pass the predicate, `P'."
(-any? p xs))
(defun list-contains? (x xs)
"Return t if X is in XS using `equal'."
(-contains? xs x))
(defun list-xs-distinct-by? (f xs)
"Return t if all elements in XS are distinct after applying F to each."
(= (length xs)
(->> xs (-map f) set-from-list set-count)))
(provide 'list)
;;; list.el ends here

View file

@ -0,0 +1,32 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Dependencies
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require 'ert)
(require 'list)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Tests
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ert-deftest list-length ()
(= 0 (list-length '()))
(= 5 (list-length '(1 2 3 4 5))))
(ert-deftest list-reduce ()
(= 16 (list-reduce 1 (lambda (x acc) (+ x acc)) '(1 2 3 4 5))))
(ert-deftest list-map ()
(equal '(2 4 6 8 10)
(list-map (lambda (x) (* x 2)) '(1 2 3 4 5))))
(ert-deftest list-xs-distinct-by? ()
(list-xs-distinct-by?
(lambda (x) (plist-get x :kbd))
'((:kbd "C-a" [:name] "foo")
(:kbd "C-b" :name "[]foo"))))
(ert-deftest list-dedupe-adjacent ()
(equal '(1 2 3 4 3 5)
(list-dedupe-adjacent '(1 1 1 2 2 3 4 4 3 5 5))))