elisp-for-python/README.org
2025-02-06 16:13:00 -08:00

393 lines
30 KiB
Org Mode
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

* Elisp Cheatsheet for Python Programmers
This document is for readers who are familiar with the Python programming language and wish to apply that knowledge to writing Emacs Lisp (Elisp). It is intended to be a “cheat sheet”/quick reference and should not be considered a primary source for either Python or Emacs APIs.
❗ This is a *work in progress*. Constructive [[https://github.com/kickingvegas/elisp-for-python/issues][feedback]] is encouraged.
Version: 0.2.1
* Table of Contents :TOC_3:
- [[#elisp-cheatsheet-for-python-programmers][Elisp Cheatsheet for Python Programmers]]
- [[#guiding-principles][Guiding Principles]]
- [[#collections][Collections]]
- [[#comparison-functions][Comparison Functions]]
- [[#sequence-types][Sequence Types]]
- [[#non-mutating-python-sequence-to-elisp-list-translations][Non-Mutating Python Sequence to Elisp List Translations]]
- [[#non-mutating-python-sequence-to-elisp-vector-translations][Non-Mutating Python Sequence to Elisp Vector Translations]]
- [[#non-mutating-python-sequence-to-elisp-sequence-translations][Non-Mutating Python Sequence to Elisp Sequence Translations]]
- [[#mutating-python-sequence-to-elisp-list-translations][Mutating Python Sequence to Elisp List Translations]]
- [[#mutating-python-sequence-to-elisp-vector-translations][Mutating Python Sequence to Elisp Vector Translations]]
- [[#mutating-python-sequence-to-elisp-sequence-translations][Mutating Python Sequence to Elisp Sequence Translations]]
- [[#map-types][Map Types]]
- [[#python-dictionary-to-elisp-hash-table][Python Dictionary to Elisp Hash Table]]
- [[#python-dictionary-to-elisp-association-list-alist][Python Dictionary to Elisp Association List (alist)]]
- [[#python-dictionary-to-elisp-property-list-plist][Python Dictionary to Elisp Property List (plist)]]
- [[#looping][Looping]]
- [[#python-string-to-elisp-string][Python String to Elisp String]]
- [[#file-io][File I/O]]
- [[#license][License]]
* Guiding Principles
- This document uses Python 3.12 and Emacs Lisp 29.1+ APIs from built-in packages.
- Python code translated to Elisp emphasizes using generic (aka [[https://en.wikipedia.org/wiki/Polymorphism_(computer_science)#:~:text=A%20function%20that%20can%20evaluate,which%20such%20specializations%20are%20made.][polymorphic]]) functions.
- This lowers the cognitive load of working with different Elisp sequence types (*list*, *vector*) and map types (*hash-table*, *alist*).
- The Emacs packages ~seq.el~ and ~map.el~ do the heavy lifting here for sequence and map types respectively.
- This document aims to provide guidance for a proficient Python programmer to implement Elisp code using familiar programming abstractions without surprise.
- Performance is at best a tertiary concern.
* Collections
** Comparison Functions
In Elisp, the comparison function used to disambiguate elements in a sequence-type collection or keys in a map-type collection is /significant/. When in doubt, it is better to specify the comparison function than trust (hope) that the default comparison function will behave to developer intent. [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Equality-Predicates.html][Info (elisp) Equality Predicates]] details the built-in comparison functions.
That said, here is some general guidance. Use the comparison function:
- ~=~ to compare numbers
- ~eq~ to compare object identity.
- ~eql~ to compare numbers but also take into account /type/. For example comparing an integer to its numerically equivalent float will return ~nil~.
- ~equal~ to compare if the objects have equal components.
| Python | Elisp | Notes |
|--------+-------+-------|
| ~is~ | ~eq~ | |
| ~==~ | ~equal~ | |
Depending on the type of object to compare, you may have to resort to writing a custom comparison function.
** Sequence Types
There are three basic sequence types in Python: list, tuple, and range. This document will cover only Elisp translation of Python list and tuple types and subsequent reference to Python sequences should be understood to not include range. For Elisp sequence types, this document will cover list and vector types.
The Elisp *list* type is a tree data structure with a /linked-list/ style abstraction for accessing its nodes. Elisp also offers a general purpose array type called a [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Vectors.html][vector]].
The following legend describes types for variables used in the translations.
#+begin_example
s: sequence
x: any
t: sequence
n: integer
i: integer
j: integer
cmp: comparison function
#+end_example
*** Non-Mutating Python Sequence to Elisp List Translations
Elisp list specific translations.
| Python Sequence | Elisp List | Notes |
|-------------------------------------+---------------------------------------------------------------+----------------------------------------------------------------|
| ~s = []~, ~s = list()~, ~s = (n1, n2, …)~ | ~(setq s ())~, ~(setq s nil)~ | ~nil~ has [[https://www.gnu.org/software/emacs/manual/html_node/eintr/nil-explained.html#:~:text=In%20Emacs%20Lisp%2C%20the%20symbol,%2C%20()%20%2C%20or%20as%20nil%20.][extra meaning]] in Elisp as it represents an empty list. |
| ~list(range(0, n))~ | ~(number-sequence 0 (1- n))~ | |
| ~s * n~ or ~n * s~ | ~(cl-loop repeat n append s)~, ~(apply #'append (make-list n s))~ | ~cl-loop~ needs ~(require 'cl-lib)~. |
| ~x in s~ | ~(member x s)~ | ~member~ can be used if ~cmp~ is ~equal~. |
| ~x not in s~ | ~(not (member x s))~ | ~member~ can be used if ~cmp~ is ~equal~. |
| ~s + t~ | ~(seq-concatenate 'list s t)~, ~(append s t)~ | |
| ~s[0]~ | ~(car s)~ | |
| ~s[-1]~ | ~(car (last s))~ | |
*** Non-Mutating Python Sequence to Elisp Vector Translations
Elisp vector specific translations.
| Python Sequence | Elisp Vector | Notes |
|-----------------+--------------------------------------------+-------|
| ~s = []~ | ~(setq s (vector))~ | |
| ~s + t~ | ~(seq-concatenate 'vector s t)~, ~(vconcat s t)~ | |
| ~s[i]~ | ~(aref s i)~ | |
*** Non-Mutating Python Sequence to Elisp Sequence Translations
These translations work on either Elisp list or vector types.
| Python Sequence | Elisp Sequence (List or Vector) | Notes |
|-------------------------+--------------------------------------+----------------------------------------------|
| ~x in s~ | ~(seq-contains-p s x #'cmp)~ | Make sure ~cmp~ will compare the element type! |
| ~x not in s~ | ~(not (seq-contains-p s x #'cmp))~ | Make sure ~cmp~ will compare the element type! |
| ~map(lambda a: a * n, s)~ | ~(seq-map (lambda (a) (* n a)) s)~ | |
| ~s[i]~ | ~(seq-elt s i)~ | |
| ~s[i:j]~ | ~(seq-subseq s i j)~ | |
| ~s[i:]~ | ~(seq-subseq s i)~ | |
| ~s[i:j:k]~ | | |
| ~len(s)~ | ~(seq-length s)~, ~(length s)~ | |
| ~min(s)~ | ~(seq-min s)~ | Elements of ~s~ must be orderable. |
| ~max(s)~ | ~(seq-max s)~ | Elements of ~s~ must be orderable. |
| ~s.index(x)~ | ~(seq-position s x)~ | |
| ~s.count(x)~ | ~(seq-count (lambda (a) (cmp x a)) s)~ | |
| ~s[0]~ | ~(seq-first s)~ | |
| ~s[-n]~ | ~(seq-first (seq-subseq s -n))~ | |
| ~if not s:~ | ~(seq-empty-p s)~ | |
*** Mutating Python Sequence to Elisp List Translations
Elisp analogs to the Python *list* methods to handle insertion, appending, and updating are left to the developer to implement. Arguably, the omission of these functions is reluctance on the part of Emacs Core to make the trade-off design decisions required to implement them.
The following Elisp translations will mutate the original input ~s~.
| Python Sequence | Elisp List | Notes |
|-----------------+----------------------------------------------------------------+--------------------------------------------|
| ~s.append(x)~ | ~(setq s (nreverse (cons x (reverse s))))~ | This implementation is slow if ~s~ is large. |
| ~s.clear()~ | ~(setq s ())~, ~(setq s nil)~ | |
| ~s.extend(t)~ | ~(setq s (append s t))~ | |
| ~s *=n~ | ~(setq s (cl-loop repeat n append s))~ | ~cl-loop~ needs ~(require 'cl-lib)~. |
| ~s.push(x)~ | ~(push x s)~ | |
| ~s.pop()~ | ~(pop s)~ | |
| ~s.insert(0, x)~ | ~(push s x)~ | |
| ~s.insert(i, x)~ | ~(setq s (append (seq-subseq s 0 i) (cons x (seq-subseq s i))))~ | |
*** Mutating Python Sequence to Elisp Vector Translations
| Python Sequence | Elisp Vector | Notes |
|-----------------+-------------------+-------|
| ~s[i] = x~ | ~(aset s i x)~ | |
| ~s.clear()~ | ~(setq s (vector))~ | |
| ~s.remove(x)~ | ~(remove x s)~ | |
*** Mutating Python Sequence to Elisp Sequence Translations
These translations work on either Elisp list or vector types.
| Python Sequence | Elisp Sequence (List or Vector) | Notes |
|-----------------+------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ~s[i] = x~ | ~(setf (seq-elt s i) x)~ | |
| ~s[i:j] = t~ | | |
| ~del s[i:j]~ | ~(setq s (append (seq-subseq s 0 i) (seq-subseq s j)))~ | |
| ~del s[i]~ | (setq s (seq-remove-at-position s i)) | |
| ~s[i:j:k] = t~ | | |
| ~del s[i:j:k]~ | | |
| ~s.copy()~ | ~(seq-copy s)~ | |
| ~s.extend(t)~ | ~(setq s (append s t))~ | |
| ~s *=n~ | ~(setq s (cl-loop for _ from 1 to n nconc (seq-copy s)))~ | |
| ~s.remove(x)~ | ~(setq s (seq-remove (lambda (a) (cmp x a)) s))~, ~(setq s (remove x s))~ | Note Elisp translation presumes only one instance of ~x~ is in ~s~, as ~seq-remove~ will remove all instances of ~x~ whereas in Python ~s.remove(x)~ will only remove the first instance. |
| ~s.reverse()~ | ~(setq s (seq-reverse s))~, ~(setq s (reverse s))~, ~(setq s (nreverse s))~ | ~nreverse~ may destructively mutate ~s~. |
** Map Types
*** Python Dictionary to Elisp Hash Table
The Elisp ~hash-table~ is the most straightforward analog to a Python dictionary. That said, there are gotchas, particularly around ~hash-table~ creation. If the keys are of type *string*, then the key comparison should be set to the function ~equal~ via the ~:test~ slot. If ~:test~ is omitted the default function ~eql~ is used which compares numbers.
#+begin_example
d: dictionary/hash-table
k: key
v: value
cmp: comparison function
#+end_example
| Python | Elisp | Notes |
|--------------------+----------------------------------------+------------------------------------------|
| ~d = dict()~, ~d = {}~ | ~(setq d (make-hash-table :test #'cmp))~ | If ~:test~ is omitted, default ~cmp~ is ~eql~. |
| ~list(d)~ | ~(map-keys d)~ | |
| ~len(d)~ | ~(map-length d)~ | |
| ~d[k]~ | ~(map-elt d k)~ | |
| ~d[k] = v~ | ~(map-put! d k v)~ | |
| ~del d[k]~ | ~(map-delete d k)~ | |
| ~k in d~ | ~(map-contains-key d k)~ | |
| ~k not in d~ | ~(not (map-contains-key d k))~ | |
| ~iter(d)~ | | |
| ~d.clear()~ | ~(clrhash d)~ | |
| ~d.copy()~ | ~(map-copy d)~ | |
| ~d.get(k)~ | ~(map-elt d k)~ | |
| ~d.items()~ | ~(map-pairs d)~ | |
| ~d.keys()~ | ~(map-keys d)~ | |
| ~d.pop(k)~ | | |
| ~d.popitem()~ | | |
| ~reversed(d)~ | | |
| ~d.values()~ | ~(map-values d)~ | |
| | ~(map-insert d k v)~ | Like ~map-put!~ but does not mutate ~d~. |
*** Python Dictionary to Elisp Association List (alist)
An *alist* is a convention to construct a basic list such that key-value semantics can be applied to it. An *alist* is allowed to possess degenerate keys (that is, keys are not necessarily unique!). This is because in truth, an *alist* is still a list with no actual enforcement of how (key, value) pairs are stored in it. IMHO Elisp *alists* are an abomination, albeit a pragmatic one. Conventional Elisp wisdom arguing for *alist* usage boils down to convenient serialization and the notion that in practice, *alist* sizes are small enough to not merit the overhead of using hash-tables.
Regardless, my guidance is to exercise caution when translating Python dictionary code to an Elisp *alist*.
#+begin_example
d: dictionary/alist
k: key
v: value
#+end_example
| Python | Elisp | Notes |
|--------------------+------------------------------+-----------------------------------------------------------------------|
| ~d = dict()~, ~d = {}~ | ~(setq d (list))~ | |
| ~list(d)~ | ~(map-keys d)~ | |
| ~len(d)~ | ~(map-length d)~ | |
| ~d[k]~ | ~(map-elt d k)~ | Type-specific behavior requires specifying test function ~t~. |
| ~d[k] = v~ | ~(map-put! d k v)~ | This only works if ~d~ is not nil. To initialize use ~(push '(k . v) d)~. |
| ~del d[k]~ | ~(setq d (map-delete d k))~ | Type-specific behavior is dependent on key type. 😞 |
| ~k in d~ | ~(map-contains-key d k)~ | |
| ~k not in d~ | ~(not (map-contains-key d k))~ | |
| ~iter(d)~ | | |
| ~d.clear()~ | ~(setq d (list))~ | |
| ~d.copy()~ | ~(map-copy d)~ | |
| ~d.get(k)~ | ~(map-elt d k)~ | |
| ~d.items()~ | ~(map-pairs d)~ | |
| ~d.keys()~ | ~(map-keys d)~ | |
| ~d.pop(k)~ | | |
| ~d.popitem()~ | | |
| ~reversed(d)~ | | |
| ~d.values()~ | ~(map-values d)~ | |
| | ~(map-insert d k v)~ | Like ~map-put!~ but does not mutate ~d~. Also does not check uniqueness. |
*** Python Dictionary to Elisp Property List (plist)
TBD
*** Looping
Two functions which can iterate through a map are ~map-do~ and ~map-apply~. Shown below are the Python translated equivalents.
#+begin_src elisp :lexical no
(map-do f d) ; return nil
#+end_src
#+begin_src python
def map_do(d):
for k,v in d.items():
f(k, v)
#+end_src
#+begin_src elisp :lexical no
(map-apply f d) ; return results of f applied to each element of d as a list
#+end_src
#+begin_src python
def map_apply(d):
results = []
for k,v in d.items():
results.append(f(k, v))
return results
#+end_src
** Python String to Elisp String
#+begin_example
s: string
a: string
b: string
c: string
sep: separator string
strs: list of strings
#+end_example
| Python | Elisp | Notes |
|-------------------------------+---------------------------------------+-------------------------------|
| ~""~ | ~(make-string 0 ? )~, ~""~ | |
| ~a + b + c~ | ~(concat a b c)~ | |
| ~s.strip()~ | ~(string-clean-whitespace s)~ | |
| ~s.capitalize()~ | ~(capitalize s)~ | |
| ~s.casefold()~ | | |
| ~s.center(width)~ | | |
| ~s.count(sub)~ | | |
| ~s.encode(encoding)~ | | |
| ~s.endswith(suffix)~ | ~(string-suffix-p suffix s)~ | |
| ~s.expandtabs(tabsize)~ | | |
| ~s.find(sub)~ | ~(string-search sub s)~ | |
| ~s.format(*args, **kwargs)~ | ~(format fmt args…)~ | |
| ~s.index(sub)~ | ~(string-search sub s)~ | |
| ~s.isalnum()~ | ~(string-match "^[[:alnum:]]*$" s)~ | |
| ~s.isalpha()~ | ~(string-match "^[[:alpha:]]*$" s)~ | |
| ~s.isascii()~ | ~(string-match "^[[:ascii:]]*$" s)~ | |
| ~s.isdecimal()~ | | |
| ~s.isdigit()~ | ~(string-match "^[[:digit:]]*$" s)~ | |
| ~s.islower()~ | ~(string-match "^[[:lower:]]*$" s)~ | ~case-fold-search~ must be nil. |
| ~s.isnumeric()~ | | |
| ~s.isprintable()~ | ~(string-match "^[[:print:]]*$" s)~ | |
| ~s.isspace()~ | ~(string-match "^[[:space:]]*$" s)~ | |
| ~s.istitle()~ | | |
| ~s.isupper()~ | ~(string-match "^[[:upper:]]*$" s)~ | ~case-fold-search~ must be nil. |
| ~sep.join(strs)~ | ~(string-join strs sep)~ | |
| ~s.ljust(width)~ | | |
| ~s.lower()~ | ~(downcase s)~ | |
| ~s.lstrip()~ | ~(string-trim-left s)~ | |
| ~s.removeprefix(prefix)~ | ~(string-remove-prefix prefix s)~ | |
| ~s.removesuffix(suffix)~ | ~(string-remove-suffix suffix s)~ | |
| ~s.replace(old, new, count=-1)~ | ~(string-replace old new s)~ | |
| ~s.rfind(sub)~ | | |
| ~s.rindex(sub)~ | | |
| ~s.rjust(width)~ | | |
| ~s.rsplit(sep)~ | | |
| ~s.rstrip()~ | ~(string-trim-right s)~ | |
| ~s.split(sep)~ | ~(split-string s sep)~ | |
| ~s.splitlines()~ | ~(string-lines s)~ | |
| ~s.startswith(prefix)~ | ~(string-prefix-p prefix s)~ | |
| ~s.strip()~ | ~(string-trim s)~ | |
| ~s.swapcase()~ | | |
| ~s.title()~ | ~(upcase-initials s)~ | |
| ~s.upper()~ | ~(upcase s)~ | |
| ~s.zfill(width)~ | | |
| ~s1 == s2~ | ~(string-equal s1 s2)~, ~(string= s1 s2)~ | |
* File I/O
The in-memory representation of a file in Emacs is a *buffer*, whose closest analog in a general purpose language like Python is a *file handle*. A common pattern is to read the contents of a file into list of strings, each string separated by a newline ("\n").
Here is an example of this in Python.
#+begin_src python
def read_file_lines(filename):
with open(filename, "r") as infile:
lines = infile.readlines()
return lines
for line in read_file_lines(filename):
print(line.rstrip('\n'))
#+end_src
Here is an Elisp equivalent.
#+begin_src elisp :lexical no
(defun read-file-lines (filename)
"Load FILENAME into a buffer and read each line."
(with-temp-buffer
;; Insert the contents of the file into the temporary buffer
(insert-file-contents filename)
;; Move to the beginning of the buffer
(goto-char (point-min))
;; Initialize an empty list to hold the lines
(let ((lines '()))
;; Loop until the end of the buffer is reached
(while (not (eobp))
;; Read the current line
(let ((line (string-trim-right (thing-at-point 'line t))))
;; Add the line to the list
(push line lines))
;; Move to the next line
(forward-line 1))
;; Return the lines in the correct order
(nreverse lines))))
;; Example usage:
(let ((lines (read-file-lines "somefile.log")))
(dolist (line lines)
(message "%s" line)))
#+end_src
Writing an Elisp list to a file is illustrated in the following example.
#+begin_src elisp :lexical no
(defun write-strings-to-file (strings filename)
"Write a list of STRINGS to FILENAME, one string per line."
(with-temp-file filename
;; Iterate over each string in the list
(dolist (str strings)
;; Insert the string followed by a newline character
(insert str "\n"))))
;; Example usage:
(let ((my-strings (read-file-lines "somefile.log"))
(file-path "some-other-file.log"))
(write-strings-to-file my-strings file-path))
#+end_src
Although the above examples work as advertised, conventional Elisp wisdom frowns upon pipeline style processing of collections arguing that:
1. Elisp has been optimized to work in-place with buffer contents and that transformations should be made directly to the buffer content.
2. Pipeline style processing of collections is slow. If you are going to process a large log file, using Elisp is not the right tool for the job.
It is left to the reader whether to heed this guidance.
* License
[[https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg]]\\
© 2025. This work is openly licensed via [[https://creativecommons.org/licenses/by/4.0/][CC BY 4.0]].