about summary refs log tree commit diff
path: root/users/wpcarro/emacs/pkgs/list
diff options
context:
space:
mode:
Diffstat (limited to 'users/wpcarro/emacs/pkgs/list')
-rw-r--r--users/wpcarro/emacs/pkgs/list/default.nix28
-rw-r--r--users/wpcarro/emacs/pkgs/list/list.el193
-rw-r--r--users/wpcarro/emacs/pkgs/list/tests.el32
3 files changed, 253 insertions, 0 deletions
diff --git a/users/wpcarro/emacs/pkgs/list/default.nix b/users/wpcarro/emacs/pkgs/list/default.nix
new file mode 100644
index 0000000000..490c0ba174
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/default.nix
@@ -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
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/list/list.el b/users/wpcarro/emacs/pkgs/list/list.el
new file mode 100644
index 0000000000..836eee89eb
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/list.el
@@ -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
diff --git a/users/wpcarro/emacs/pkgs/list/tests.el b/users/wpcarro/emacs/pkgs/list/tests.el
new file mode 100644
index 0000000000..2f7090d467
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/tests.el
@@ -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))))